December 3, 2020
by Viktor Kovacs
modified at December 17, 2020

Archicad Maze Generator Add-On Tutorial – Part 2

In the previous part of this tutorial series we’ve learned how to create BIM elements in Archicad, but we used hard-wired values for settings. In this part we will create a user interface to allow users to define maze generation settings.

The code for this tutorial is available here.

The tutorial is divided into multiple parts:

The final dialog will look like this:

Dialog Framework (DG)

To create user interface in Archicad it is highly recommended to use Archicad’s own Dialog framework (DG). It has several advantages:

  • Platform Independent: You have to write your code only once, and it will work on Windows and macOS platforms without any modification.
  • Handles working units: All of the DG dialogs handle working unit settings, so your dialog will work for people from all around the world.
  • Fits in Archicad: All dialogs in Archicad are written with DG, so they will provide a native-like user experience for Archicad users. You don’t have to care about formatting, they will just look beautiful.

Creating a New Dialog

Let’s see how to create a new modal dialog in Archicad. Basically it takes takes three steps:

  1. Define the resource for the dialog.
  2. Write the code for the dialog.
  3. Open the dialog, and use the results.

Define the Resource for the Dialog

The first step is to define the resource for the dialog. You can do it by using Archicad’s internal resource format: grc. This is a platform independent format for describing strings, dialogs, icons and other resources.

Our dialog’s resource will look like this:

'GDLG' ID_ADDON_DLG Modal          40   40  250  452  "Maze Settings" {
/* [  1] */ Button                150  419   90   23    LargePlain  "OK"
/* [  2] */ Button                 50  419   90   23    LargePlain  "Cancel"
/* [  3] */ Icon                   15   10  220  160    10002
/* [  4] */ LeftText               10  180  230   23    LargeBold vCenter "Grid Settings"
/* [  5] */ LeftText               10  210  130   23    LargePlain vCenter "Number of Rows"
/* [  6] */ PosIntEdit            150  210   90   23    LargePlain "1" "50"
/* [  7] */ LeftText               10  240  130   23    LargePlain vCenter "Number of Columns"
/* [  8] */ PosIntEdit            150  240   90   23    LargePlain "1" "50"
/* [  9] */ LeftText               10  270  130   23    LargePlain vCenter "Cell Dimension"
/* [ 10] */ LengthEdit            150  270   90   23    LargePlain "1.00" "50.0"
/* [ 11] */ Separator              10  305  230    2
/* [ 12] */ LeftText               10  317  230   23    LargeBold vCenter "Options"
/* [ 13] */ CheckBox               10  347  230   23    LargePlain "Group placed elements"
/* [ 14] */ CheckBox               10  372  230   23    LargePlain "Place slab under walls"
/* [ 15] */ Separator              10  407  230    2
}

It looks a bit scary at first, but its structure is pretty straightforward.

The first line tells the compiler that it defines a dialog resource (GDLG) for a modal dialog. It also specifies the position, the dimensions and the title of the dialog. After that every line defines a control. All of them specifies the position (top left corner), dimensions (width and height) and some control-dependent properties.

Here are some interesting parts:

  • If the dialog has OK and Cancel buttons, they always have to be the first two items, so the default behavior for pressing enter and escape key works automatically when the dialog is open.
  • For Icon controls you have to specify the ID of the icon. It usually refers to a GICN resource in another, language-independent resource file.
  • LeftText is simply a plain static text control with left-aligned text.
  • The PosIntEdit control can accept only integers, and the minimum and maximum value can be specified as well.
  • LengthEdit is used to display a value referring to length. It is highly recommended to use this control when you work with length values, because it automatically handles unit conversion. The minimum and maximum value can be specified here, too.

For dialog resources there is always a connected resource, and it’s the help resource (DLGH). The goal of the help resource is to define help anchors for the dialog. These help anchors are not used by Add-Ons at the moment, but they can also define the tooltip text for every control. In the example below every tooltip text is empty.

'DLGH' ID_ADDON_DLG DLG_Maze_Settings {
1  ""  Button_0
2  ""  Button_1
3  ""  Icon_0
4  ""  LeftText_0
5  ""  LeftText_1
6  ""  PosIntEdit_0
7  ""  LeftText_2
8  ""  PosIntEdit_1
9  ""  LeftText_3
10 ""  LengthEdit_0
11 ""  Separator_0
12 ""  LeftText_4
13 ""  CheckBox_0
14 ""  CheckBox_1
15 ""  Separator_1
}

The Observer Pattern

The dialog framework uses the observer pattern. It means that observer classes can attach to event source classes, and they will get a notification in case of certain events. It’s possible to write dialog and observer classes separately, but in this tutorial we will do it in one class.

Write the Code for the Dialog

The first step is to create a structure to store everything that can be set via the dialog. The MazeSettings class will store the row and column count and the size of maze cells. We also define two options: one is to group all of the elements, and the other is to place a slab under the elements.

class MazeSettings : public GS::Object
{

public:
	MazeSettings (UInt32 rowCount, UInt32 columnCount, double cellSize, bool createGroup, bool createSlab);
	~MazeSettings ();

	UInt32	rowCount;
	UInt32	columnCount;
	double	cellSize;
	bool	createGroup;
	bool	createSlab;
};

The next step is to write a class for the dialog. The dialog class will get the initial settings in the constructor, and it has only one public function that returns the new settings based on user input. The dialog class should be inherited from several classes:

  • ModalDialog: It defines some functionalities needed for a modal dialog.
  • PanelObserver: It means that our dialog wants to respond to some panel events.
  • ButtonItemObserver: Same as above, our dialog wants to respond to some button events (specifically: pressing the OK or the Cancel button).
  • CompoundItemObserver: It’s a utility class to make it easy to attach all observers with one line.

It also defines some private class members for each control in the dialog, and of course it stores the current settings.

class MazeSettingsDialog :  public DG::ModalDialog,
                            public DG::PanelObserver,
                            public DG::ButtonItemObserver,
                            public DG::CompoundItemObserver
{
public:
    MazeSettingsDialog (const MazeSettings& mazeSettings);
    ~MazeSettingsDialog ();

    const MazeSettings& GetMazeSettings () const;

private:
    virtual void    PanelOpened (const DG::PanelOpenEvent& ev) override;
    virtual void    PanelCloseRequested (const DG::PanelCloseRequestEvent& ev, bool* accepted) override;
    virtual void    ButtonClicked (const DG::ButtonClickEvent& ev) override;

    DG::Button      okButton;
    DG::Button      cancelButton;
    DG::PosIntEdit  rowEdit;
    DG::PosIntEdit  columnEdit;
    DG::LengthEdit  cellSizeEdit;
    DG::CheckBox    groupElementsCheck;
    DG::CheckBox    placeSlabCheck;

    MazeSettings    mazeSettings;
};

Let’s see how it works. The constructor intializes all of the controls with their ids, stores the initial settings, and it attaches the class as an observer to certain event sources. The destructor detaches the class from event sources.

MazeSettingsDialog::MazeSettingsDialog (const MazeSettings& mazeSettings) :
	DG::ModalDialog (ACAPI_GetOwnResModule (), MazeDialogResourceId, ACAPI_GetOwnResModule ()),
	okButton (GetReference (), OKButtonId),
	cancelButton (GetReference (), CancelButtonId),
	rowEdit (GetReference (), RowEditId),
	columnEdit (GetReference (), ColumnEditId),
	cellSizeEdit (GetReference (), CellSizeEditId),
	groupElementsCheck (GetReference (), GroupElementsCheckId),
	placeSlabCheck (GetReference (), PlaceSlabCheckId),
	mazeSettings (mazeSettings)
{
	AttachToAllItems (*this);
	Attach (*this);
}

MazeSettingsDialog::~MazeSettingsDialog ()
{
	Detach (*this);
	DetachFromAllItems (*this);
}

When the dialog is fully initialized, the PanelOpened function will be called. This is the place to initialize controls. In this example the function sets every settings value into the corresponding dialog control.

void MazeSettingsDialog::PanelOpened (const DG::PanelOpenEvent&)
{
	rowEdit.SetValue (mazeSettings.rowCount);
	columnEdit.SetValue (mazeSettings.columnCount);
	cellSizeEdit.SetValue (mazeSettings.cellSize);
	groupElementsCheck.SetState (mazeSettings.createGroup);
	placeSlabCheck.SetState (mazeSettings.createSlab);
}

When the user clicks on a button, the ButtonClicked function will be executed. After clicking on the OK or the Cancel button, it will request to close the dialog with different return codes.

void MazeSettingsDialog::ButtonClicked (const DG::ButtonClickEvent& ev)
{
	if (ev.GetSource () == &okButton) {
		PostCloseRequest (DG::ModalDialog::Accept);
	} else if (ev.GetSource () == &cancelButton) {
		PostCloseRequest (DG::ModalDialog::Cancel);
	}
}

Before the dialog closes, the PanelCloseRequested function will be called. This is a good place to modify settings values based on the controls. We only have to do this if the event is accepted (the user clicked on OK).

void MazeSettingsDialog::PanelCloseRequested (const DG::PanelCloseRequestEvent& ev, bool*)
{
	if (ev.IsAccepted ()) {
		mazeSettings.rowCount = rowEdit.GetValue ();
		mazeSettings.columnCount = columnEdit.GetValue ();
		mazeSettings.cellSize = cellSizeEdit.GetValue ();
		mazeSettings.createGroup = groupElementsCheck.IsChecked ();
		mazeSettings.createSlab = placeSlabCheck.IsChecked ();
	}
}

Open the Dialog

Now we have all the code for the dialog so we can use it. Opening a modal dialog is very simple. We just have to create an instance, call the Invoke method, and check if the user clicked OK or Cancel.

The function below opens the dialog, and stores the settings in an output parameter. If the dialog is closed by pressing OK it will return true, otherwise it will return false. It also defines the initial settings for the dialog so it will be filled with valid values after it is opened.

static bool GetMazeSettingsFromDialog (MazeSettings& mazeSettings)
{
	MazeSettings initialMazeSettings (10, 20, 1.0, true, true);
	MazeSettingsDialog mazeSettingsDialog (initialMazeSettings);
	if (mazeSettingsDialog.Invoke ()) {
		mazeSettings = mazeSettingsDialog.GetMazeSettings ();
		return true;
	} else {
		return false;
	}
}

Summary

This was only the top of the iceberg. Dialog creation has much more possibilities, like creating resizable, dockable palettes, extend existing dialogs and a lot more. You can find more details in the documentation that comes with the API Development Kit.

You can find more resources here: