Autodesk 3ds Max ships with PySide 2.0 and includes all standard PySide modules. PySide2 is the preferred framework for building UIs with Python in 3ds Max.
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.
In most cases you will need a parent widget for your PySide UI, which is the 3ds Max main window. The way to get the QWidget version of the 3ds Max main window is this:
from pymxs import runtime
from PySide2.QtWidgets import QWidget
main_window_qwdgt = QWidget.find(runtime.windows.getMAXHWND())
Widgets parented to the main widget will behave predictably, minimizing when the main window is minimized, and deleted properly when 3ds Max exits. To display the widget as modeless, call .show()
, and to open it as modal, call .exec_()
. Widgets parented to the QWidget version of the 3ds Max main window are not dockable.
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.
To enable docking (such as in the example below), you can use the QMainWindow
returned by the qtmax.GetMaxMainWindow()
convenience function.
The following 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 qtmax
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)
qtmax.DisableMaxAcceleratorsOnFocus(self, True)
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'))
main_window = qtmax.GetQMaxMainWindow()
w = PyMaxDockWidget(parent=main_window)
w.setFloating(True)
w.show()
if __name__ == '__main__':
main()
Some notes on this example:
Though not strictly necessary, this example sets the window flags to QtCore.Qt.Tool
to improve the appearance of the dialog, and sets the attribute QtCore.Qt.QA_DeleteOnClose
to delete the dialog immediately on close. Without this flag, the dialog is hidden, but not deleted when the close button is clicked.
We disable 3ds Max accelerator keys using qtmax.DisableMaxAcceleratorsOnFocus()
. Though not required in this example, it is useful if you want to define your own keyboard shortcuts.
Objects created in your script need to be protected from being garbage collected. There are two approaches you can take. One is to keep references to them in a static class variable. The other is to inherit from a QWidget
class (in this example, QDockWidget
), which will hold the static reference, and since Qt now manages the lifecycle of the widget, it is protected from being garbage collected.
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.QtWidgets import QDialog
from PySide2.QtWidgets import QPushButton
from PySide2.QtCore import QFile
from PySide2.QtUiTools import QUiLoader
from pymxs import runtime as rt
class TestDialog(QDialog):
def __init__(self, parent=QWidget.find(rt.windows.getMAXHWND())):
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(ui_file_path)
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(QPushButton, 'pushButton')
btn.clicked.connect(self.makeTeapot)
def makeTeapot(self):
rt.teapot()
rt.redrawViews()
def main():
dlg = TestDialog()
dlg.show()
if __name__ == '__main__':
main()
The qtmax
module contains the LoadMaxMultiResIcon()
helper function that you can use to load multi-resolution icons from 3ds Max, from a custom compiled Qt rcc resource, or from the file system.
This example loads two standard 3ds Max multi-resolution icons and displays them on QButtons.
import PySide2
from PySide2.QtWidgets import QMainWindow, QDialog, QHBoxLayout, QToolButton
from PySide2.QtCore import QSize
from qtmax import GetQMaxMainWindow, LoadMaxMultiResIcon
class TestDialog(QDialog):
def __init__(self, parent):
super(TestDialog, self).__init__(parent)
self.setWindowTitle('Test Dialog')
self.setAttribute(PySide2.QtCore.Qt.WA_DeleteOnClose)
main_layout = QHBoxLayout()
toolBtn1 = QToolButton()
toolBtn1.setIcon(LoadMaxMultiResIcon("Common/Lock"))
toolBtn1.setIconSize(QSize(48, 48))
main_layout.addWidget(toolBtn1)
toolBtn2 = QToolButton()
toolBtn2.setIcon(LoadMaxMultiResIcon("Common/UnLock"))
toolBtn2.setIconSize(QSize(48, 48))
main_layout.addWidget(toolBtn2)
self.setLayout(main_layout)
self.resize(400, 200)
def executeTestDialog():
main_window = GetQMaxMainWindow()
# Create our Qt test dialog
dlg = TestDialog(main_window)
dlg.show()
executeTestDialog()