Creating a Qt-based plug-in

Once you have installed Qt 5 and the Qt VS Tools for Visual Studio 2017 you can start using Qt to build UIs for your 3ds Max plug-ins. See the SDK Requirements topic for the Qt version required for your target version of 3ds Max.

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.

Note:

You can also use Qt for plug-ins that are not param-block based, for example UtilityObj plug-ins. In that case your wrapper class will derive from QWidget instead of QMaxParamBlockWidget, and you add it to the Max UI using the version of AddRollupPage() that takes a QWidget pointer. You can specify the ROLLUP_DONT_ADD_TO_CP flag for rollups that do not reside on the command panel, for example for a floating Qt dialog.

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\objects .

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 2017.

Under Qt VS Tools > Qt Options, add the target Qt version, with a path pointing to the installed version of Qt.

We need to manually edit the project file, to convert it to a Qt project so that the Visual Studio Qt extension will recognize it.

Open the gsphere.vcxproj file in a text editor. Find the "Globals" PropertyGroup element, and after the <ProjectGuid> element, add this element: <Keyword>Qt4VS</Keyword>

At the bottom of the file, before the final <Project> element add:

  <ProjectExtensions>
    <VisualStudio>
      <UserProperties Qt5Version_x0020_x64="5.12.5" />
    </VisualStudio>
  </ProjectExtensions>

Save the project file and re-load the project in Visual Studio.

Note:

Qt VS Tools presents the option to convert the project from a "custom build steps" project to a Qt/MSBuild project. For this tutorial we will stay with the custom build steps method, but Qt/MSBuild is supported. See the <maxsdk>\samples\materials\CameraMapTexture\CameraMapTexture.vcxproj sample project to see how to configure a project this way.

Under Qt VS Tools > Qt Project Settings, select the Qt Modules tab, and check Core, GUI and Widgets modules. This will add the appropriate Qt libraries to the additional dependencies for each configuration.

The final step we need to take to upgrade our project is to add $(QTDIR)\include under Properties > C/C++ > Additional Include Directories, and $(QTDIR)\lib under Properties > Linker > Additional Library Directories.

Select Qt VS Tools > Launch Qt 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 objectName properties 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". In Qt Designer, select both buttons, right click, and select Assign to button group. Rename the QButtonGroup "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. You can force it to compile by right-clicking on the associated .ui file and selecting Compile.

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 a moc file is now auto-generated:

Once the files are configured correctly, you can right-click .ui files and select "compile" in Visual Studio to actually generate the layouts header moc files based on your design.

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

Import GeosphereCreationType.h in GSPHERE.cpp.

Move the definition of the block ID enums (geo_creation_type, etc) to the top of the file.

Change the target output name to gsphereqt.

Note:

Be sure to add MaxQtBridge.lib to the Linker > Input > Additional Dependencies for the project.

Compile the project. Load the plug-in in 3ds Max and confirm that our rollout is being created by Qt:

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:

Optional: You can tweak the layout a bit in Qt Designer to get slightly better results. In our example, the spin boxes look a bit narrow, and appear to float over to the left side of the rollup. This is because the grid layout has placed them in the second column of the grid, and the checkboxes below cause the first column to be quite wide. To make the layout look a bit better:

The 3ds Max SDK provides some customized Qt widgets that will work better in the Max UI. 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::QmaxDoubleSpinBox" in Promoted class name.

Enter "Qt/qmaxspinbox.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::QmaxDoubleSpinBox".

You can also promote the "Segments" spinbox to a MaxSDK::QMaxSpinBox using a similar procedure.

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.

In Visual Studio, 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 GeosphereParameters 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 QmaxDoubleSpinBox, 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.