Using Qt in 3ds Max 2018 Utility Plug-ins

Login to Follow
  • Film & VFX
  • Design Visualization
  • Programming
  • Plug-ins
  • 3ds Max
Skill Level
  • Advanced
30 min

This tutorial will cover how to use Qt to create the UI for UtilityObj Plug-ins. Most plug-in types use parameter blocks, and therefore require a UI based on MaxSDK::QMaxParamBlockWidget. UtilityObj plug-ins do not require this, in fact, we can base our UI directly on QWidget. We can also take advantage of some of the other functionality provided by the Qt toolkit.

We'll walk through creating a plug-in that uses the socket library of Qt to allow external editors to send 3ds Max python and MAXScript scripts to execute.

Note: Complete source for this example can be found at

Create the Project

Create a new UtilityObj project in Visual Studio 2015, using the 3ds Max Plug-in Wizard, which will set up paths and property sheets.

Follow the general instructions in this tutorial to get Qt support set up for your Plug-in Project. In step 5, add the Network module as well, as we're going to use Qt sockets for communication.

Create the UI

Use the Widget template to create a new UI. Our Plug-in UI is pretty simple, a Push Button to start and stop the server, and Plain Text Edit to display status. I laid mine out with a grid layout, and made these changes:

  • QWidget objectName = “server_rollout”
  • QPushButton objectName = “button_start”, text =
    “Start Server”, checkable = true
  • QPlainTextEdit objectName = “text_status”

It should look something like this:

Save the file in the project directory as “server_rollout.ui”. Close the Qt Designer, and add the file to your VS project.
Because the project is a Qt VS Tools project, the .ui file is recognized as a form, and added to a new “Form Files” folder, and an associated .h file created in the “Generated Files” folder:

Create a UI Wrapper Class

There are a couple of ways to use Qt .ui files in C++ projects, as the Qt docs cover ( We’re using the “Single Inheritance Approach” described there, where we create a “wrapper” class for the UI (inheriting from QObject), and adding our logic there.

Add a new Qt class to the project, inherit from QWidget (the base class of our UI), and name it something like “Server_Rollup_Wrapper”. This will add the .cpp and .h files, and also associated moc files (see for information about the Qt meta-object compiler).

Try to build the project. At this point your project should build, though it doesn’t do anything.

First, let’s set up the main plug-in code. See the listing for MaxScriptServer.cpp. Notice a couple of things:

  • It includes the wrapper header and , because it holds a pointer to our QWidget-based UI.
  • It uses a version of AddRollupPage() that takes a QWidget instead of a HWND.
  • There’s no UI logic here, we handle things like button presses in the wrapper.

Now let’s set up the wrapper code. First, look at the Server_Rollup_Wrapper.h listing. Here’s what’s going on there:

  • We declare a server_rollup class in the Ui namespace, which, if you look in ui_server_rollup.h, is declared there based on the form we built and saved in the server_rollup.ui file.
  • There’s a Q_OBJECT macro that tells the Qt moc compiler to process this class.
  • We declare some slots, which Qt uses to connect events (“signals” in Qt speak, such as pressing a button) with handlers (“slots” in Qt terms).
  • We have a pointer to our UI class, and a QTcpServer and QtcpSocket to listen for scripts.

Let’s take a closer look at the implementation in Server_Rollup_Wrapper.cpp.

Server_Rollup_Wrapper::Server_Rollup_Wrapper(QWidget *parent)
: QWidget(/*parent*/),
ui(new Ui::server_rollup)

In the constructor we create a new server_rollup, and call setupUi() to instantiate the form and its widgets.

void Server_Rollup_Wrapper::Server_Rollup_Wrapper::on_btnStart_toggled(bool tog)
if (tog)
ui->text_status->setPlainText("Server Started");
tcpServer.listen(QHostAddress::LocalHost, 9999);
ui->text_status->appendPlainText(tr("Listening on port: %1").arg(tcpServer.serverPort()));
connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));


ui->text_status->setPlainText("Server Stopped");
disconnect(&tcpServer, 0, 0,0);

This is an event handler for toggling the button. The moc compiler looks for slots with the format “on_[widget_name]_[widget_event]” and wires them up. In this case we’re handling the toggling of
the push button (which can be toggled because its “checkable” property is true). The Boolean tells us if the
button is down (true) or up (false). If it’s pressed, we tell the server to start listening, and connect a handler
(acceptConnection) for new connections.

void Server_Rollup_Wrapper::acceptConnection()
tcpServerConnection =
connect(tcpServerConnection, SIGNAL(readyRead()), this, SLOT(readData()));
ui->text_status->appendPlainText("Accepted Connection");


Here we accept a new connection by creating a tcpServerConnection object, and connecting a handler to read any data when it’s ready.

void Server_Rollup_Wrapper::readData()
QString res = QString(tcpServerConnection->readAll());

// for testing:
// this->ui->text_status->appendPlainText(res);

FPValue result;
const wchar_t* buffer;
buffer = res.toStdWString().c_str();
TCHAR fileExension[MAX_PATH];
SplitFilename(buffer, NULL, NULL, fileExension);
bool success;
QString result_str ="";
TSTR script;
// Are we running a python script?
if (_tcscmp(fileExension, _T(".py")) == 0 || _tcscmp(fileExension, _T(".pyw")) == 0)
script.printf(_T("clearListener(); python.executeFile(@\"%s\")"), buffer);
else // assume we're a mxs script:
script.printf(_T("clearListener(); filein @\"%s\" quiet:false"), buffer);

// Execute the script and check for success
success = ExecuteMAXScriptScript(script, false, &result);

// if we failed, let's send back the errors from the listener
if (!success)
// try to get the listener output:
TSTR mxs_script("setListenerSel #(0,-1); ListenerText=getListenerSelText(); setListenerSel #(-1,-1); ListenerText");
ExecuteMAXScriptScript(mxs_script, true, &result);

if (result.type && result.type == TYPE_STRING)
result_str = QString::fromWCharArray(result.s);

if (result.type && result.type == TYPE_TSTR)
WStr s = result.tstr->ToWStr();
result_str = QString::fromWCharArray(s);
QString success_str = success ? "Succeeded" : "Failed";
QString success_msg = QString("Result: " + success_str + "\r\n" + result_str + "\r\n");

This is where we read the data and process it. We’re expecting a fully-qualified script file name. If it’s a python script, we create a MAXScript Python.ExecuteFile() command for it, and pass that to ExecuteMAXScriptScript(). Otherwise we assume it’s a MAXScript, and use filein() to execute it. If the execution fails, we run a bit of MAXScript to capture the errors on the listener output and send it back. Otherwise, we send a success message.

Note – you could send the whole script over the wire instead of just the script name, if you wanted to send from another machine on the network. In this case we’re sticking to localhost for simplicity and security.

Ok, that part is done, you should be able to compile and load the plug-in in Max. Run max, load the plug-in, and click the “Start Server” button to confirm that it’s working:

Create the Script Sender

The second, easier bit is a “sender” application that takes a script filename as a command line parameter, and sends it to the port Max is listening on.

Have a look at the ScriptSender project in the MaxScriptServer solution. We don’t need to look at it line by line -- this is a simple Qt console app that takes a script file name, and sends it to Max, waits for a response, and writes the response to stdout. The response will be displayed in the Editor terminal or build output window.

Note that we need to manually copy over a couple of Qt dlls to the output directory for this application to work – we use a couple of post-build steps in the project to do this:

xcopy /y /d
"D:\Qt\5.6\msvc2015_64\bin\Qt5Network.dll" "$(OutDir)"

xcopy /y /d
"D:\Qt\5.6\msvc2015_64\bin\Qt5Core.dll" "$(OutDir)"

Test It Out

Let’s test it out with VS Code, my new favourite text editor. Note that VS Code has a couple of packages that support MAXScript currently, some just syntax highlighting, others with code completion. It’s worth trying them out.

We’ll configure a custom build task (see Configure a new task of type “other”. Note – tasks are associated with open directories, so open a directory containing some scripts first.

Here’s my tasks.json:

// See
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "D:\\github\\max-samples\\MaxscriptServer\\x64\\Release\\ScriptSender.exe",
"isShellCommand": true,
"args": ["-f", "${file}"],
"showOutput": "always"

Now open, or create a script, and Ctrl-Shift-B to “build” (which will call our task), and confirm that it’s running in Max:

And that’s it. This solution will work for any editor that allows you to define a custom build command. A more advanced version of this tool would enable debugging, as VS Code has a debug extension type:, and perhaps we’ll tackle that in another blog post.

Posted By
  • 3ds Max
  • Programming
  • Plug-ins
To post a comment please login or register
| 3 years ago
Quite interesting. Even more interesting is to find the _only_ place in the whole Google that mentions what ScriptSender.exe actually does. I wonder why Autodesk fails to mention that _anywhere_. Perhaps I'm not searching hard enough. (and the '-f' switch doesn't work for me for some reason)
| 1 year ago
Hi Drew, how about the promised VSCode debugging? VS code is also more and more getting my favourite editor.. Istan
| 1 year ago
Hi Istan, we are working on some tutorials for external editors, keep tuned.