3ds Max 2017 introduced several great improvements to its Python scripting support. In this article, we’ll look at some of the new features around using PySide for scripted UIs.
The first item is something that has been long requested – a way to parent Qt/PySide to the Max main window. It is accomplished using MaxPlus.GetQMaxWindow() (or MaxPlus.AttachQWidgetToMax(), introduced in 3ds Max 2016 Extension 1).
Secondly, general PySide support has been greatly improved. PySide version 1.2.2 is now included with the full library set, not just the core.
Finally, 3ds Max now includes the pyside-uic utility “in the box,” which makes loading Qt forms designed in Qt Designer/Creator much easier. This is what we’re going to look at in more detail, by creating a simple UI in Qt Designer and wiring it up to a Python script running in Max. If you are new to Qt Designer, it is an application that allows you to easily create Qt-based user interfaces using drag and drop.
3ds Max 2017 supports Qt 4.8.5 / PySide 1.2.2, so you should install a version of Qt Designer compatible with these versions, especially if you want to also create Qt UIs for your C++ plug-ins.
Open Qt Designer and create a new form using the Widget template.
Add a few items to the widget by dragging them onto the form. You don’t need to place them exactly, you’ll use some layouts to do that later.
I’ve added a push button, a group box containing two radio buttons, a slider, a dial, a date edit and a line edit (both with labels). You can add them too, but the only widgets you really need for this tutorial are the button and the date edit.
As mentioned before, to get things to line up nicely you’ll need to apply some layouts. First, select the group box and click the Layout Vertically button.
Next, click on an empty area to select the form, and then apply a form layout.
Finally, click on the Adjust Size button to reduce the unused space.
Your results should look something like mine.
Note that generally you want to name widgets, but for this quick tutorial you can just use the default names for simplicity.
There are a lot of Qt Designer tutorials out there, check them out to learn more about this tool.
That’s pretty much it – save the .ui file in your 3ds Max scripts directory, and switch over to 3ds Max.
Once you are in Max, open the MAXScript Editor and create a new, empty Python script.
Enter the following script:
import os, MaxPlus
from PySide import QtCore, QtGui
fname = os.path.join(os.path.abspath(os.path.curdir), "my_ui.ui")
ui_type, base_type = MaxPlus.LoadUiType(fname)
print ui_type
print base_type
def buttonClicked():
print "pushButton clicked"
class MyWidget(base_type, ui_type):
def __init__(self, parent = None):
base_type.__init__(self)
ui_type.__init__(self)
self.setupUi(self)
MaxPlus.AttachQWidgetToMax(self)
pb = self.pushButton
pb.clicked.connect(buttonClicked)
form = MyWidget()
form.show()
Save and run this script from the same directory as the .ui file you created; the Qt Form will then appear as a dialog in 3ds Max, complete with 3ds Max styling. When you click the PushButton, a “clicked” message appears in the Listener.
So what magic is happening here? First, the script gets the running directory (os.path.curdir) and looks for the my_ui.ui file saved there. On startup, the current directory will be the 3ds Max running directory, but it changes to your script directory when you open or save a new script to that location.
Note: We don’t use usual method of looking at file to find the current script directory because when you evaluate a script in the MAXScript Editor, file is not defined. If you absolutely need __file__, always run your script from the Scripting > Run Script… command, and it will be available in that context.
Next, the script passes the .ui file to MaxPlus.LoadUiType(), which is a wrapper for pyside-uic.exe. It converts the .ui file into a .py file in memory, executes it, and returns a tuple containing the Qt UI type (“Ui_Form”) and base class (“PySide.QtGui.QWidget”). With these two items, you can create one or more instances of the UI in your script.
These two items are passed to the constructor for your wrapper class, MyWidget; the script calls __init__(self) on each before calling setupUi(). Then, it attaches to the main Max window and finally, attaches a callback to the button’s “clicked” event.
This is all pretty straightforward. But what if you want to pass data in from the form? Let’s pass the date from the DateEdit widget in a callback, and print that out.
First, let’s check what signal gets emitted from the QDateEdit in the Qt documentation:
http://doc.qt.io/qt-5/qdatetimeedit.html#dateChanged. dateChanged(const QDate &date) looks like the right one – it contains a reference to the new value of the widget as a QDate.
Add this function to the script:
def printDate(datetime):
print datetime.toString()
And hook it up to our QDateEdit in the definition for MyWidget():
self.dateEdit.dateChanged[QtCore.QDate].connect(printDate)
The key here is to match the PySide.QtCore data type with the type in the signal signature, QDate.
Save and run, and you should see the date printed to the Listener every time you change its value.
I hope this short tutorial shows you how easy it is to create nice looking UIs for your 3ds Max Python scripts using Qt Designer and PySide.