Creating A Maya PySide2 Widget in 2023

[TL;DR Simple tutorial in building Maya widgets, focusing on solid working practices.]

This is something as technical artist we have to do all the time. It’s easy enough to get a widget showing in Maya, but there are a couple of hoops to jump through so that the widget gets parented to the Maya application. Over the years, I’ve refined my method for setting widgets up, so what I’m thinking is that it might be useful to show some of my widget building strategies.

Getting A Pointer To The Maya Window

We need to get this reference, so we can set our widget within the Maya window hierarchy. I’ve seen a few elaborate ways to get the pointer, but recently, I’ve settled for assigning it as a single line constant.

from PySide2.QtWidgets import QWidget
from maya.OpenMayaUI import MQtUtil
from shiboken2 import wrapInstance


MAYA_MAIN_WINDOW: QWidget = wrapInstance(int(MQtUtil.mainWindow()), QWidget)

The Widget Class

I do a few things with the widget to make my life easier. While I can demonstrate that with the code below, bear in mind that I often put this code into a generic widget, and build new tools by subclassing that general widget, so that I can avoid boiler-plate code. Also note that because this is a short example, I’m putting the events into the same class as the ui. Typically, I would separate those elements and use a model-view-controller format. The example tool gives us a way of querying the current time and date.

import platform
from datetime import datetime

from PySide2.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel, QSizePolicy
from PySide2.QtCore import Qt
from maya.OpenMayaUI import MQtUtil
from shiboken2 import wrapInstance
from typing import List, Type


MAYA_MAIN_WINDOW: QWidget = wrapInstance(int(MQtUtil.mainWindow()), QWidget)


class ExampleMayaWidget(QWidget):
def __init__(self):
"""
Example of a Maya PySide2 widget which parents to the main window
"""
super(ExampleMayaWidget, self).__init__(parent=MAYA_MAIN_WINDOW)
self.setWindowFlags(Qt.Tool if platform.system() == "Darwin" else Qt.Window)
self.setWindowTitle("Example Maya Widget")
layout = QHBoxLayout()
layout.setMargin(2)
layout.setSpacing(2)
self.setLayout(layout)
self.button: QPushButton = self.add_widget(QPushButton("Get Date/Time"))
self.button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
self.label: QLabel = self.add_widget(QLabel("< click me"))
self.setup_connections()
self.setStyleSheet("font-size: 10pt")
self.setFixedSize(400, self.sizeHint().height())

def setup_connections(self):
"""
Setup events
"""
self.button.clicked.connect(lambda: self.label.setText(f"Date/time: {self.date_time}"))

@property
def date_time(self) -> str:
return datetime.now().strftime('%m/%d/%Y, %H:%M:%S')

def add_widget(self, widget: QWidget) -> QWidget:
"""
Convenience function for adding widgets
:param widget: QWidget
:return: QWidget
"""
self.layout().addWidget(widget)
return widget

Things to note here:

  • Docstrings used on each function.
  • Typing used, which really helps with debugging.
  • Convenience function, add_widget() to add a widget with a single command.
  • For a simple command, I connect the click event using a lambda function.
  • Setting the Windows flags is crucial for ensuring that the widget is properly parented to the Maya application.

Getting The Widget Instance

Another area which I’ve tinkered with over the years is my strategy for launching tools. Introducing my latest trick. I use a function, get_widget() to search for an existing instance of my widget class before going ahead and creating a new instance. This ensures that the widget retains its previous state if recalled, and guards against multiple versions of your widget without you having to force delete the widget prior to creation.

def get_widget(widget_class: Type[QWidget], first_only: bool = True) -> Type[QWidget] or List[Type[QWidget]] or None:
    """
    Finds instances of the passed widget classes in Maya
    @param widget_class: the widget class
    @param first_only: set to true to only pass the first instance
    @return:
    """
    if first_only:
        return next((x for x in MAYA_MAIN_WINDOW.children() if type(x) is widget_class), None)
    else:
        return [x for x in MAYA_MAIN_WINDOW.children() if type(x) is widget_class]

The Launch Script

So here’s how I use the launch script to show the tool in Maya. The get_widget() function is passed the class itself, so no messing around looking for tools variable names in the global variables.

from maya_playground.example_maya_widget import ExampleMayaWidget, get_widget

example_maya_widget = get_widget(widget_class=ExampleMayaWidget)
if not example_maya_widget:
    example_maya_widget = ExampleMayaWidget()
example_maya_widget.show()

As previously mentioned, this is just a self-contained example. To avoid boiler-plating the code, elements like the Maya main window pointer and the get_widget() function can be placed in a separate utilities modules. And the ui building tech can be set up in a generic class that can be subclassed for convenience. Any questions, about any of this, get in contact!

Note that in the screenshot, I also used a reference to importlib.reload(), and while that function is useful during the debugging process, I would not use it in a final production-ready tool. Reload forces the Python interpreter to create new temporary binary files to run the code from, and that can sometimes mess things up. It’s not necessary when a tool is complete, but it’s handy when you have to make small changes to your code to be able to get an updated tool in Maya.

Leave a comment