Interfaz nativa de Maya dentro de PySide2 (Maya 2017+)

Con las nuevas versiones de PySide2 y Qt5 en Maya 2017+, la manera de empotrar (o como suele decirse a veces, “embeber”, que viene del inglés “embed”) interfaz nativa de Maya (usando maya.cmds) en interfaz de Qt, concretamente PySide2, ya no se hace de la misma manera.

Conocía las versiones para PyQt de algunos blogs conocidos como el blog de Justin Israel (aka JustinFX) y su post del 2011 que aún seguía vigente hasta Maya 2016 para empotrar interfaz de Maya en PyQt4, o este post en el blog de Nathan Horne, o incluso ejemplos de GitHub como este, pero no podía encontrar una versión actualizada para PySide que, a mi parecer, es la manera de proceder a partir de Maya 2017 por varias razones: está incluido en la instalación de Maya, es prácticamente igual, es más pequeño puesto que ya no incluye muchas funcionalidades deprecadas del API 1 de PyQt, y además ahora forma parte de la wiki oficial de Qt. Digamos que PyQt se ha convertido en una vieja gloria. Ya escribiré sobre esto en otro artículo.

Anteriormente la manera de proceder era utilizando Shiboken (o Sip para PyQt), haciendo unwrap (desenvolviendo) la instancia del layout de Qt del que disponíamos convirtiéndolo en una dirección / puntero y luego obteniendo el nombre del objeto correspondiente en Maya usado el API.

obj = mui.MQtUtil.fullName(long(shiboken2.unwrapInstance(qtObj)))

Pero esto ya no funcionará para Maya 2017+.

Las diferencias principales son:

  • Hay que usar shiboken2, incluso cuando la documentación oficial dice que se usa shiboken (envié un mensaje a Autodesk para que lo arreglaran hace unos pocos días y ahora que lo compruebo de nuevo veo que lo han arreglado!)
  • No existe la función unwrapInstance. Tendrás que hacer esto utilizando la función de shiboken2 llamada getCppPointer.

Aquí tenéis el ejemplo para hacerlo, donde doy por hecho que ya existe una camara “persp” en la escena. Podría ser cualquier otra.

from PySide2 import QtWidgets
import maya.cmds as cmds
import maya.OpenMayaUI as mui
import shiboken2

# Creamos una ventana simple con un widget
window = QtWidgets.QWidget()
window.resize(500,500)

# Tenemos nuestro layout de Qt donde queremos insertar, por ejemplo, un viewport
qtLayout = QtWidgets.QVBoxLayout(window)

# Primero usamos shiboken para obtener el puntero del layout.
# Devuelve dos valores pero solo nos interesa el primero.
layout = long(shiboken2.getCppPointer(qtLayout)[0])

# Usamos el API de Maya para obtener el nombre en Maya a partir del punero
layoutName = mui.MQtUtil.fullName(layout)

# Establecemos ese nombre como padre de la interfaz para continuar creando
# interfaz nativa de Maya usando maya.cmds. 
cmds.setParent(layoutName)

# Creamos, por tanto, el layout necesario para introducir el viewport
paneLayoutName = cmds.paneLayout()
    
# Creamos un modelPanel. Usamos # para generar un nombre único para el nuevo panel y evitar conflictos de nombres
modelPanelName = cmds.modelPanel("embeddedModelPanel#", cam="persp")
    
# Ahora que está creada la interfaz, obtenemos el puntero usando el Maya API
ptr = mui.MQtUtil.findControl(paneLayoutName)
    
# Envolvemos el puntero en un objeto QWidget. Fijaos que para sip se usa un QObject y en shiboken se usa un QWidget
paneLayoutQt = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)

# Ahora que ya tenemos un widget, lo incluimos en nuestro layout previo.
qtLayout.addWidget(paneLayoutQt)

window.show()

El nombre “layoutName” será probablemente ‘|’ en un ejemplo sencillo como este, lo que significa que es “la raíz”. Si le diésemos un nombre al objeto Qt usando setObjectName(“nombre”), tendríamos una ruta “nombre|” en su lugar.

Si tu intención es insertar este viewport en otro lugar más profundo en la jerarquía de layouts de Qt, el API te devolverá ‘|||’ o algo similar, que devolverá un error al intentar establecer esto como el padre de la interfaz con setParent.

La manera en que resuelvo esto es siempre utilizar ‘|’ como el layoutName, así que no tienes que añadir las primeras lineas del código anterior. Lo he probado y funciona, sea donde sea el lugar donde quieres incluir interfaz nativa.

Se convierte en esto:

from PySide2 import QtWidgets
import maya.cmds as cmds
import maya.OpenMayaUI as mui
import shiboken2

# Creamos una ventana simple con un QWidget
ventana = QtWidgets.QWidget()
ventana.resize(500,500)
# Tenemos nuestro layout de Qt donde queremos insertar, por ejemplo, un viewport
qtLayout = QtWidgets.QVBoxLayout(ventana)

# Establecemos la raíz como padre de la interfaz nativa. Creamos el paneLayout necesario para introducir el viewport dentro.
cmds.setParent('|')
paneLayoutName = cmds.paneLayout()
    
# Creamos un modelPanel. Usamos # para generar un nombre único para el nuevo panel y evitar conflictos de nombres
modelPanelName = cmds.modelPanel("embeddedModelPanel#", cam="persp")
    
# Ahora que está creada la interfaz, obtenemos el puntero usando el Maya API
ptr = mui.MQtUtil.findControl(paneLayoutName)
    
# Envolvemos el puntero en un objeto QWidget. Fijaos que para sip se usa un QObject y en shiboken se usa un QWidget
paneLayoutQt = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)

# Ahora que ya tenemos un widget, lo incluimos en nuestro layout previo.
qtLayout.addWidget(paneLayoutQt)

ventana.show()

EDITADO:

De nuevo, usar ‘|’ como padre ha vuelto a devolver errores en otros casos que he probado y después de investigar un poco he encontrado que la manera de hacer que funcione mejor es dándole un nombre al objeto Qt:

qtLayout.setObjectName('viewportLayout')

Y luego utilizar ese nombre como padre, en vez de usar la jerarquía de Maya con símbolos |.

cmds.setParent('viewportLayout')

Lo he probado con un par de ventanas ejecutándose a la vez y los nombres, sorprendentemente, no tienen conflicto y funciona perfectamente. Esta parte no tiene mucho sentido y todavía estoy preguntándome cómo es posible que Maya pueda encontrar el objeto usando setParent. En cuanto sepa algo actualizaré el artículo.

Este sería el código final:

from PySide2 import QtWidgets
import maya.cmds as cmds
import maya.OpenMayaUI as mui
import shiboken2

# Creamos una ventana simple con un QWidget
ventana = QtWidgets.QWidget()
ventana.resize(500,500)
# Tenemos nuestro layout de Qt donde queremos insertar, por ejemplo, un viewport
qtLayout = QtWidgets.QVBoxLayout(ventana)

# Le damos un nombre al layout en Qt
qtLayout.setObjectName('viewportLayout')

# Establecemos el layout como padre de la nueva interfaz de Maya usando el nombre utilizado antes.
cmds.setParent('viewportLayout')

#Creamos el paneLayout necesario para introducir el viewport dentro.
paneLayoutName = cmds.paneLayout()
    
# Creamos un modelPanel. Usamos # para generar un nombre único para el nuevo panel y evitar conflictos de nombres
modelPanelName = cmds.modelPanel("embeddedModelPanel#", cam="persp")
    
# Ahora que está creada la interfaz, obtenemos el puntero usando el Maya API
ptr = mui.MQtUtil.findControl(paneLayoutName)
    
# Envolvemos el puntero en un objeto QWidget. Fijaos que para sip se usa un QObject y en shiboken se usa un QWidget
paneLayoutQt = shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)

# Ahora que ya tenemos un widget, lo incluimos en nuestro layout previo.
qtLayout.addWidget(paneLayoutQt)

ventana.show()

El resultado es este:

 

Nota: He experimentado problemas con el nombrado automático utilizando # en el pasado. A veces Maya es incapaz de encontrar un nombre generando números para sustituir ese # y que sea un nombre único, pero de momento para este ejemplo no he tenido problemas. Si tienes problemas con esto, recomiendo usar un truco sencillo pero efectivo: importamos el módulo “time” de Python, y utilizamos time.time() para obtener un timestamp del tiempo actual en milisegundos. Luego añadimos este número al nombre, de esta manera el nombre será único con toda probabilidad.

Leave a Reply

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *