Creating a Qt-based plug-in

Once you have the Autodesk-patched version of Qt 4.8.5 and the Qt Manager add-in for Visual Studio 2012 you can start using Qt to build UIs for your 3ds Max plug-ins.

3ds Max plug-ins will recognize Qt widgets and hook them into your code when configured properly. In general:

NOTE:The plug-in must use IParamBlock2, IParamBlock does not support Qt integration.

In this topic, we’ll walk through updating an existing traditional plug-in to use Qt for its rollouts. This will illustrate the basics of using Qt in the 3ds Max plug-in environment. We are going to use the Qt Designer to create our Qt forms; although you can do this by hand, it is much easier and faster to use the Designer.

NOTE:A final version of the code in this tutorial is available on the ADN github: https://github.com/ADN-DevTech.

We are going to work on the Geodesic Sphere plug-in, located in masdk\samples\object.

Make a copy of the gsphere directory and name it gsphereqt. To simplify the process, locate the gsphereqt directory in the <maxsdk>\samples\objects directory, the same directory as gsphere

Open the gsphereqt\gsphere.vcxproj in Visual Studio 2012.

Under QT4MAX > Qt Options, add a Qt version 4.8.5, with a path pointing to the Autodesk-patched version of Qt 4.8.5.

Select QT4MAX > Convert project to Qt Add-in project.

We need to manually edit the project file, to add additional link dependencies to each target configuration. Open the file in a text editor. Find the generic <ItemDefinitionGroup>, with a <link><AdditionalDependencies> element that looks like this:

<Link>
      <AdditionalDependencies>comctl32.lib;delayimp.lib;paramblk2.lib;core.lib;edmodel.lib;geom.lib;gfx.lib;maxutil.lib;mesh.lib;%(AdditionalDependencies)</AdditionalDependencies>
      <AdditionalLibraryDirectories>$(MaxSdkLib);$(QTLIB);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
      <ModuleDefinitionFile>.\PRIM.DEF</ModuleDefinitionFile>
      <DelayLoadDLLs>edmodel.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
</Link>

Move the <AdditionalDependencies> element into the <ClCompile> element for the following <ItemDefinitionGroup> elements that specify the debug, hybrid, and release configurations.

Save the file and reload it in Visual Studio 2012.

Under QT4MAX > Qt Project Settings, check Core and GUI modules. This will add QtCore_Ad_d4.lib and QtGui_Ad_d4.lib to the additional dependencies for each configuration.

Select QT4MAX > Launch Designer, and create a new form based on the Widget template. This is our first rollout, and should look something like this:

We don’t need to create the title label, just the radio buttons. Add two radio buttons and select Layout in a Grid and then Adjust Size. Change the text labels to "Diameter" and "Center", and the radio button object names to "diameter" and "center" respectively.

The radio buttons need to be part of a Qt RadiobuttonGroup with a name that matches a corresponding param name, and the widget itself should match the name of a param block. In this case, we want to hook up the "creationMethod" param to the radio buttons, and the widget with the "GeosphereCreationType" paramblock:

static ParamBlockDesc2 geo_crtype_blk ( geo_creation_type, _T("GeosphereCreationType"), 0, &gsphereDesc, P_CLASS_PARAMS + P_AUTO_UI, 
	//rollout
	IDD_GSPHERE1, IDS_CREATION_METHOD, BEGIN_EDIT_CREATE, 0, NULL,
	// params
	geo_create_meth,  _T("creationMethod"), 		TYPE_INT, 		0, IDS_CREATION_METHOD, 	 
		p_default, 		1, 
		p_range, 		0, 1, 
		p_ui, 			TYPE_RADIO, 	2, IDC_CREATEDIAMETER, IDC_CREATERADIUS, 
		p_end, 
	p_end
	);

So, we need to name the widget "GeosphereCreationType", and add the radio buttons to a RadioButtonGroup named "creationMethod". It should look something like this:

Save the form as gsphere_creation_type.ui in the project folder, and then add it to the Resource Files in the Solution Explorer in Visual Studio. Because this is a Qt Add-in project, Visual Studio recognizes the UI file and creates an associated header for it:

Note: this header file does not yet exist, because the .ui file has not been compiled. We’ll do that later in Visual Studio 2015.

Next, we will add a wrapper class for our new rollout. Add a new class to the project, and name it GeosphereCreationType.

Edit GeosphereCreationType.h to look like this:

#pragma once
#include <maxapi.h>
#include <Qt/QMaxParamBlockWidget.h>

// This namespace is defined in the header generated by Qt
namespace Ui {
	class GeosphereCreationType;
};

// Inherit from QMaxParamBockWidget
class GeosphereCreationType :public MaxSDK::QMaxParamBlockWidget
{
	// Connects us to Qt's meta-object system
	Q_OBJECT

public:
	explicit GeosphereCreationType(QWidget* parent = nullptr);
	~GeosphereCreationType(void);

	// Virtual functions required by QMaxParamBockWidget
	virtual void SetParamBlock(ReferenceMaker* owner, IParamBlock2* const param_block) {};
	virtual void UpdateUI(const TimeValue t) {};
	virtual void UpdateParameterUI(const TimeValue t, const ParamID param_id, const int tab_index) {};
	
private:
	Ui::GeosphereCreationType* ui;

};

The Q_OBJECT macro causes Visual Studio to recognize this class as part of the Qt system, and will generate the associated source and .moc files. We also have to implement some virtual methods from QMaxParamBlockWidget.

Edit GeosphereCreationType.cpp to look like this:

#include "GeosphereCreationType.h"
#include "ui_gsphere_creation_type.h"

GeosphereCreationType::GeosphereCreationType(QWidget* parent)
	:QMaxParamBlockWidget(/*parent*/),
	ui(new Ui::GeosphereCreationType())
{
	ui->setupUi(this);
	// map radio button IDs to ints in the paramblock:
	// If a button is clicked, the button group will emit a change
	// which will trigger a change in the param block, and vice versa.
	ui->creationMethod->setId(ui->diameter, 0);
	ui->creationMethod->setId(ui->center, 1);
}


GeosphereCreationType::~GeosphereCreationType(void)
{	delete ui; }

Here we set up the ids of the creationMethod Radio Button Group, which is required to hook it up to the creationMethod parameter.

Visual Studio should recognize that our new wrapper class is a Q_OBJECT and to generate Qt moc files for it. Check that the moc files are now auto-generated:

We can now save the project and re-open it in Visual Studio 2015.

Right-click the gsphere_creation_type.ui file, and select Compile. You should now be able to open and verify that ui_gsphere_creation_type.h is generated.

Finally, we need to add an implementation of CreateQtWidget(). In GSPHERE.CPP, add this function to the GsphereClassDesc definition:

virtual MaxSDK::QMaxParamBlockWidget* CreateQtWidget(
		ReferenceMaker& /*owner*/,
		IParamBlock2& paramBlock,
		const MapID paramMapID,
		MSTR& rollupTitle,
		int& rollupFlags,
		int& rollupCategory) {

		if (paramBlock.ID() == geo_creation_type) {
			// this invokes the Qt translation system:
			//: this is a comment for the translation team 
			rollupTitle = GeosphereCreationType::tr("Creation Method (Qt)");
			return new GeosphereCreationType();
		}
		return nullptr;

	};

Here we set the rollout title. Note the special //: comment format, which is recognized by the Qt translation system. You will need to reorganize the block ID enums and move them above this code.

Finally, let’s modify the geo_crtype_blk param block definition itself to connect to Qt. Delete the rollout definition, and change P_AUTO_UI to P_AUTO_UI_QT. It should look like this:

static ParamBlockDesc2 geo_crtype_blk ( geo_creation_type, _T("GeosphereCreationType"), 0, &gsphereDesc, P_CLASS_PARAMS + P_AUTO_UI_QT, 
	// params
	geo_create_meth,  _T("creationMethod"), 		TYPE_INT, 		0, IDS_CREATION_METHOD, 	 
		p_default, 		1, 
		p_range, 		0, 1, 
		p_end, 
	p_end
	);

We also need to change the ClassID and dll name so they do not conflict with the existing GeoSphere plug-in. Redefine GEOSPHERE_CLASS_ID:

#define GEOSPHERE_CLASS_ID 277485056
	 

Change the target output name to gsphereqt.

NOTE:Be sure to add MaxQtBridge.lib to the additional libraries for the project.

Switch back to Visual Studio 2015 and compile. Load the plug-in in 3ds Max and confirm that our rollout is being created by Qt:

A few notes about this workflow:

Now let’s add the Parameters rollout.

Create a new widget, add a Double Spin Box, a regular Spin Box, three radio buttons in a group box, and five check boxes. Let’s add a button to illustrate how to hook up slots, even though there isn’t one on the original roll-out. Remember to add a ButtonGroup named "baseType" (to associate it with the baseType parameter), and add all the radio buttons to it. Finally, add labels for the spin boxes, and lay them out approximately like they appear on the rollout we’re trying to re-create:

Select the group box, and apply a grid layout. Apply a grid layout to the whole widget, and then select Adjust Size. The final layout should look like this:

The 3ds Max SDK provides some customized Qt widgets that will work better in the Max UI (note that these widgets are experimental and will change in a future release - they are not intended for production). For example, the customized spin boxes will respect user decimal place settings. The headers for these widgets are located in maxsdk\include\Qt. Your project must link against MaxQtBridge.lib to use these widgets. To convert the Radius spinbox, right-click on it and select "Promote To…"

Enter "MaxSDK::QMaxFloatSpinBox" in Promoted class name.

Enter "Qt/qmaxfloatspinbox.h" in Header File.

Check Global include.

Click Add.

If you had other float spin boxes in the widget, you could convert them by right-clicking and selecting "Promote To > MaxSDK::QMaxFloatSpinBox".

Rename the widgets to match their corresponding parameter names. Name the button "sayHi". Finally, rename the form "GeosphereParameters" to match the name of the wrapper class we will create next.

Save the form as gsphere_parameters.ui in the gsphereqt project directory.

Open the project in Visual Studio 2012 and add the new form to the resources directory.

Let’s add the wrapper class for this rollout. Create a new class and name it "GeosphereParameters".

This class is going to look a lot like the previous class:

In GeosphereParameters.h:

#pragma once
#include <maxapi.h>
#include <iparamb2.h>
#include <iparamm2.h>
#include <baseinterface.h>
#include <Qt/QMaxParamBlockWidget.h>

namespace Ui {
	class GeosphereParameters;
};

class GeosphereParameters :public MaxSDK::QMaxParamBlockWidget
{
	Q_OBJECT

public:
	explicit GeosphereParameters(QWidget* parent = nullptr);
	~GeosphereParameters();

	// boilerplate
	virtual void SetParamBlock(ReferenceMaker* owner, IParamBlock2* const param_block) {};
	virtual void UpdateUI(const TimeValue t) {};
	virtual void UpdateParameterUI(const TimeValue t, const ParamID param_id, const int tab_index) {};

protected slots:
	void on_sayHi_clicked();

private:
	Ui::GeosphereParameters* ui;
};

The slots keyword is a Qt mechanism. Using the Qt signal-slot mechanism and a special naming convention (on_<NAME_OF_MY_WIDGET>_<NAME_OF_THE_SIGNAL>() ) here allows us to easily connect a button-click to that function. This connection is setup during ui->setupUI(this) - see the next code block.

And in GeosphereParameters.cpp:

#include "GeosphereParameters.h"
#include "ui_gsphere_parameters.h"


GeosphereParameters::GeosphereParameters(QWidget* parent)
	:QMaxParamBlockWidget(/*parent*/),
	ui(new Ui::GeosphereParameters())
{
	ui->setupUi(this);
	// map radio button IDs to ints in the paramblock:
	// If a button is clicked, the button group will emit a change
	// which will trigger a change in the param block, and vice versa.
	ui->baseType->setId(ui->octa, 0);
	ui->baseType->setId(ui->tetra, 1);
	ui->baseType->setId(ui->icosa, 2);
	
}
void GeosphereParameters::GeosphereParameters::on_sayHi_clicked() {
	QMessageBox::information(this, "Hi", "Hi from Qt");
}

GeosphereParameters::~GeosphereParameters()
{
	delete ui;
}

Add a new condition in GSphereClasDesc::CreateQtWidget() to handle the new rollout creation:

		if (paramBlock.ID() == geo_params) {
			rollupTitle = GeosphereParameters::tr("Parameters (Qt)");
			return new GeosphereParameters();
		}

And finally edit the param block for keyboard entry, changing P_AUTO_UI to P_AUTO_UI_QT, and remove the rollout definition and p_ui definitions:

static ParamBlockDesc2 geo_param_blk ( geo_params, _T("GeosphereParameters"),  0, &gsphereDesc, P_AUTO_CONSTRUCT + P_AUTO_UI_QT, PBLOCK_REF_NO, 

	// params
	geo_hemi, 	_T("hemisphere"),		TYPE_BOOL, 		P_ANIMATABLE,				IDS_HEMI,
		p_default, 		FALSE, 
		p_end, 
	geo_segs, 	_T("segs"), 			TYPE_INT, 		P_ANIMATABLE, 	IDS_RB_SEGS, 
		p_default, 		4, 
		p_range, 		MIN_SEGMENTS, MAX_SEGMENTS, 
		p_end, 
	geo_radius,  _T("radius"), 			TYPE_FLOAT, 	P_ANIMATABLE + P_RESET_DEFAULT, 	IDS_RB_RADIUS, 
		p_default, 		0.0,	
		p_ms_default,	25.0,
		p_range, 		MIN_RADIUS, MAX_RADIUS, 
		p_end, 
	geo_type, 	_T("baseType"),			TYPE_INT, 		0,				IDS_BASETYPE,
		p_default, 		2, 
		p_range, 		0, 2, 
		p_end, 
	geo_basepivot, 	_T("baseToPivot"), 	TYPE_BOOL, 		0,				IDS_BASEPIVOT, 
		p_default, 		FALSE, 
		p_end, 
	geo_smooth, 	_T("smooth"), 		TYPE_BOOL, 		P_ANIMATABLE,				IDS_RB_SMOOTH,
		p_default, 		TRUE, 
		p_end, 
	geo_mapping, 	_T("mapCoords"), 	TYPE_BOOL, 		0,				IDS_MAPPING,
		p_default, 		TRUE, 
		p_end, 
	p_end
	);

If you compile and run, you’ll will see our new rollouts, but notice that changing parameters on a gsphereQt object in modify mode does not update the object in the viewport. We will need to use a ParamBlock Accessor to detect param changes and trigger a redraw.

Add a PBAccessor after the GSphereClassDesc:

class GeoPBAccessor : PBAccessor {
	virtual void Get(PB2Value& v, ReferenceMaker* owner, ParamID id, int tabIndex, TimeValue t, Interval &valid) override
	{
		Interface* ip = GetCOREInterface();
		ip->RedrawViews(t, REDRAW_NORMAL);
	}
};

Create a static instance of this accessor:

static GeoPBAccessor _accessor;
	 

And add the accessor to each parameter in the GeospheereParameters paramblock:

p_accessor, &_accessor, 

At this point we have a working plug-in that uses Qt for two of its rollouts, and we’ve seen how to hook up widgets such as spinners, checkboxes and radio buttons to ParamBlocks. We’ve seen how to use the Max widget extensions, such as QMaxFloatSpinBox, and we’ve also seen how to react to Qt signals such as button clicks. To further explore Qt in Max, have a look at the maxsdk\samples\systems\sunlight plug-in, which is entirely Qt-based.