QtQuick/QML in Maya

In this post, I will discuss how to use QtQuick/QML for tools inside of Maya and whether it should be used in production. Spoiler alert: it shouldn't.

QtQuick is the module of Qt used to write QML applications. Using the QtQuick module allows designers and developers to create modern, fluid, and animated user interfaces such as this:

qml_ui

or this useful example:

qml_ui2

or something more professional and useful such as Telegram.

There are many articles on the internet discussing the differences and history between QtQuick and QtWidgets so I won't go over that in depth here. In a nutshell, QtQuick has typically been used to create mobile, touch-friendly interfaces with nice fluid animations. However, QtQuick2, introduced in Qt5, provided many significant improvements and allowed desktop apps to take advantage of the higher performance and modern features of QtQuick interfaces.

Maya and many other DCC packages began converting to Qt right before Qt5 was released. Since QtQuick1 was not a good solution for desktop applications, just about all DCC applications that use Qt use QtWidgets. Autodesk also ended up doing some custom development in Qt for bug fixes and behavioral adjustments to get Maya exactly how they wanted so they have a custom build that isn't the same as what we would get if we installed Qt from the Qt site.

Obtaining the QML Modules

As a result of having a custom build and Maya not using any QtQuick, Maya does not ship with any of the QML modules such as Qt Quick Control and Qt Quick Particles. To get access to these modules, we have to get a version of Qt compatible with our version of Maya. Usually this consists of compiling Qt using the same compiler as Maya. For example on Windows, Maya 2018 and 2019 are compiled with Visual Studio 2015 and Maya 2020 is compiled with Visual Studio 2017. Luckily, for Maya 2020 on Windows, the Qt 5.12 that you download from the Qt site is compiled with Visual Studio 2017, so we can just grab files from that installation. If there is a version or compiler mismatch with what you download, you'll need to compile Qt from source.

Expose the QML Modules to Maya

The QML modules are located in <QTDIR>/qml. We can tell QML in Maya to look in this directory by setting the QML2_IMPORT_PATH environment variable to point to this directory.

A runtime approach is to call QQmlEngine::addImportPath. Although I would typically use the runtime approach for modules included with specific interfaces you develop.

Create a Container Widget Owned By Maya

Most PySide tutorials on the web say that to display a QML interface, you do something like the following:

from PySide2.QtWidgets import QApplication
from PySide2.QtQuick import QQuickView
from PySide2.QtCore import QUrl

app = QApplication([])
view = QQuickView()
url = QUrl("view.qml")
view.setSource(url)
view.show()
app.exec_()

In Maya, we don't need to create a QApplication so we may think we can just do something like:

view = QQuickView()
url = QUrl("view.qml")
view.setSource(url)
view.show()

Doing this will cause the interface window to flash briefly and then disappear. This is because we need the Maya main interface window to own this new window and QtWidget applications can only own QWidgets.

Maya is a QtWidget-based application. To use QtQuick interfaces inside a QtWidget application, we have two choices:

  1. Use QQuickWidget
  2. Use QQuickView or QQuickWindow mixed with QWidget::createWindowContainer

QQuickWidget has a minor performance hit so I'll use QQuickView in this post. To display a QtQuick interface in a QtWidget application, we need to embed it inside a QWidget:

from maya.app.general.mayaMixin import MayaQWidgetBaseMixin

class QuickWindow(MayaQWidgetBaseMixin, QMainWindow):
    """A window that can be used to display a QML UI in Maya."""

    def __init__(self, url=None, *args, **kwargs):
        super(QuickWindow, self).__init__(*args, **kwargs)
        self.view = QQuickView()
        if isinstance(url, string_types):
            url = QUrl.fromLocalFile(os.path.abspath(url))
        self.view.setSource(url)
        size = self.view.size()
        self.widget = QWidget.createWindowContainer(self.view, self)
        self.setCentralWidget(self.widget)
        self.setFocusProxy(self.widget)
        self.resize(size)
        self.setFocusPolicy(Qt.NoFocus)

We create a container widget to hold the QQuickView and embed that widget into a QMainWindow. The window is parented to the main Maya window because we use the MayaQWidgetBaseMixin.

The *Focus* methods are to handle some focus issues I read about in a presentation from Telltale Games (RIP) Senior Tools Engineer, Michael Eads.

Removing ApplicationWindow

A lot of sample QML starts with the following:

ApplicationWindow {
    id: window
    width: 1280
    height: 720
    visible: true

    ...
}

If we try to show a QML file with an ApplicationWindow, we will see the following message

# Warning: root : QQuickView does not support using windows as a root item. 

If you wish to create your root window from QML, consider using QQmlApplicationEngine instead.

ApplicationWindow is processed by the QML engine to create a new window. However, we are already creating a window with QQuickView which is required to show the QtQuick interface in a QtWidget application. We can replace ApplicationWindow with another Container Control. I recommend using Page because it also supports a header and footer just like ApplicationWindow.

Sending Signals from QML to Python

To send Signals from the interface and respond to them in Python, we need to create a class derived from QObject that serves as the layer between the front and back end of the UI:

from PySide2.QtCore import Signal, QObject, Property

class Backend(QObject):
    value_changed = Signal(float)

    def __init__(self, parent=None):
        super(Backend, self).__init__(parent)
        self._value = 0

    @Property(float, notify=value_changed)
    def value(self):
        return self._value

    @value.setter
    def set_value(self, value):
        if self._value == value:
            return
        self._value = value
        self.value_changed.emit(self._value)

To tell QML about this class, we pass an instance into the QQmlContext of the QQmlEngine associated with our QQuickView:

class QuickWindow(MayaQWidgetBaseMixin, QMainWindow):

    def __init__(self, url=None, *args, **kwargs):
        super(QuickWindow, self).__init__(*args, **kwargs)
        self.view = QQuickView()
        ...

    def setContextProperty(self, name, value):
        self.view.engine().rootContext().setContextProperty(name, value)


def on_value_change(value):
    cmds.setAttr("pCube1.s", value, value, value)


window = QuickWindow()
window.backend = Backend() # Monkey patch to the window to prevent garbage collection
window.setContextProperty("backend", window.backend)
window.backend.value_changed.connect(on_value_change)

We can then use this property in our QML:

slider.onPositionChanged: backend.value = slider.valueAt(slider.position)

To download the sample code and some examples, check out my GitHub. The repo contains the QML modules for Maya 2020 on Windows, so if you install the Maya module in that repository, all the paths should be set up automatically and QML2_IMPORT_PATH will be set to the QML module directories within the repo.

Conclusion

QML allows for the creation of “modern” UIs with fluid animations. However, I would not hesitate to say that the majority of the user base that would use these interfaces in a desktop environment does not need or want their UI to be touch friendly or have a “modern” flair. That is not to say these users do not want clean, user-friendly interfaces. In production, we usually just want our tools to be fast and easy to use. Making them pretty with animations is usually secondary. Also using QtQuick inside Maya will mean all our interfaces will look disjointed from the rest of Maya. While QML does expose the ability to customize the look of components to match Maya, I'm not sure if it is worth the effort. These user requirements, combined with the fact that the full suite of QML modules is not bundled with Maya, lead me to recommend not using QML with Maya.

comments powered by Disqus