Creating Python UIs

Autodesk 3ds Max ships with a pre-built version of PySide 2.0 compatible with Python 2.7.15. This version includes all standard PySide modules.

Note: Because 3ds Max is single-threaded, the main UI is not updated while a Python script is running, even if it changes the scene, unless a UI update is requested. For example, a new scene object will not appear in the viewport until the script exits or pymxs.runtime.redrawViews() is called.

Creating a Dockable Widget

The following simple example shows how to create a dockable widget that is parented to and docks with the 3ds Max main window:

'''
Demonstrates how to create a QDockWidget with PySide2 for use in 3ds Max
'''

from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
import shiboken2
from pymxs import runtime as rt

def make_cylinder():
    cyl = rt.Cylinder(radius=10, height=30)
    rt.redrawViews()

    return    
    
class PyMaxDockWidget(QtWidgets.QDockWidget):
    def __init__(self, parent=None):
        super(PyMaxDockWidget, self).__init__(parent)
        self.setWindowFlags(QtCore.Qt.Tool)
        self.setWindowTitle('Pyside Qt  Dock Window')
        self.initUI()
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        
    def initUI(self):
        main_layout = QtWidgets.QVBoxLayout()
        label = QtWidgets.QLabel("Click button to create a cylinder in the scene")
        main_layout.addWidget(label)

        cylinder_btn = QtWidgets.QPushButton("Cylinder")
        cylinder_btn.clicked.connect(make_cylinder)
        main_layout.addWidget(cylinder_btn)
        widget = QtWidgets.QWidget()
        widget.setLayout(main_layout)
        self.setWidget(widget)
        self.resize(250, 100)

def main():
    rt.resetMaxFile(rt.name('noPrompt'))
    # Cast the main window HWND to a QMainWindow for docking
    # First, get the QWidget corresponding to the Max windows HWND:
    main_window_qwdgt = QtWidgets.QWidget.find(rt.windows.getMAXHWND())
    # Then cast it as a QMainWindow for docking purposes:
    main_window = shiboken2.wrapInstance(shiboken2.getCppPointer(main_window_qwdgt)[0], QtWidgets.QMainWindow)
    w = PyMaxDockWidget(parent=main_window)
    w.setFloating(True)
    w.show()

if __name__ == '__main__':
    main()

Notes:

Loading Qt UI Files

PySide2 provides a facility for loading UI files created by Qt Designer: QUiLoader. This example looks for a ui file named test_ui.ui located in the same directory as the running script and loads it. For more information, see QUiLoader.

import os
from PySide2.QtWidgets import QVBoxLayout
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import QFile
from PySide2.QtUiTools import QUiLoader
from pymxs import runtime as rt

class TestDialog(QtWidgets.QDialog):
    def __init__(self, parent=QWidget.find(rt.windows.getMAXHWND())):
        QtWidgets.QDialog.__init__(self, parent)
        loader = QUiLoader()
        # ui_file_path = os.path.join(  os.path.dirname(os.path.realpath(__file__)), 'test_ui.ui')
        ui_file = QFile('test_ui.ui')
        ui_file.open(QFile.ReadOnly)
        self.ui = loader.load(ui_file, self)
        ui_file.close()
        
        layout = QVBoxLayout()
        layout.addWidget(self.ui)
        self.setLayout(layout)
        btn = self.ui.findChild(QtWidgets.QPushButton, 'pushButton')
        btn.clicked.connect(self.makeTeapot)
        
    def makeTeapot(self):
        rt.teapot()
        rt.redrawViews()

def main():
    dlg = TestDialog()
    dlg.show()


if __name__ == '__main__':
    main()

Getting the QApplication Instance

There may be cases where you need the QApplication instance for 3ds Max. In a typical PySide2 script, you would use this code:

from PySide2 import QtWidgets
app = QtWidgets.QApplication.instance()

Normally this would return a QtWidgets.QApplication object, but because of the way 3ds Max manages Qt, this code returns a QtCore.QCoreApplication object when run in the 3ds Max Python interpreter. The solution is to use Shiboken2 to cast the QCoreApplication object to a QtWidgets.QApplication object:

from PySide2 import QtWidgets
import shiboken2

app = shiboken2.wrapInstance(shiboken2.getCppPointer(QApplication.instance())[0], QtWidgets.QApplication)