December 17, 2020
by Viktor Kovacs
modified at January 5, 2021

Archicad Maze Generator Add-On Tutorial – Part 3

In the previous parts of this tutorial series we’ve learned how to create BIM elements, and a very simple dialog. The annoying thing about the dialog is that it keeps using the default data, and doesn’t remember the previous settings. In this part we will solve this issue.

The code for this tutorial is available here.

The tutorial is divided into multiple parts:

The end result will work like this:

You can also try if you can solve it in BIMx.

Storing Data in Preferences

Archicad API provides support to store data in several places:

  • The preferences file: The data will be linked to the user’s computer, not the database. It means it will be the same regardless the actual plan file the user is working on.
  • The Archicad plan file: This method links data to the database itself so it will act like any other BIM data (e.g. it will be uploaded to BIM server).
  • Associated to BIM elements: The data will act as a core data of BIM elements (e.g. the element should be reserved to modify this data).
  • Stored in library parts: The data will be stored in a special tagged section of the library part, and “travel” with it.

When storing the state of the user interface, storing data in preferences is usually the best choice. It won’t affect the database, or any other users in a teamwork project. Saving to preferences usually takes these steps:

  1. Serialize your data to a binary blob.
  2. Write the binary data to preferences.
  3. Read the binary data from preferences.
  4. Deserialize binary blob to your data.

Serializing Data

Archicad doesn’t know anything about the Add-On, or its data structure, so an Add-On should pass a binary blob (“black box”) to Archicad. It means that the first step should be to convert your data to and from a binary blob. The good news is that the Archicad API has a pretty good support to achieve this.

Now we will prepare our MazeSettings class to make it serializable to and deserializable from a binary format. To achieve this it should be inherited from the GS::Object class that provides basic functionalities for serialization. We also have to use the DECLARE_CLASS_INFO macro that assigns some version information to the class. It can be very useful if we want to modify the data structure while keeping compatibility with previous versions.

The header file for MazeSettings will look like this:

class MazeSettings : public GS::Object
{
	DECLARE_CLASS_INFO;

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

	virtual	GSErrCode Read (GS::IChannel& ic) override;
	virtual	GSErrCode Write (GS::OChannel& oc) const override;

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

We also have to declare version information for that class in its .cpp file. We have to provide a name, a guid, and a class version.

GS::ClassInfo MazeSettings::classInfo (
	"MazeSettings",
	GS::Guid ("B45089A9-B372-460B-B145-80E6EBF107C3"),
	GS::ClassVersion (1, 0)
);

The most important part is the Read and Write functions of the class. These are responsible to serialize and deserialize the data. Both functions get an abstract channel as a parameter, and we just have to use its Read or Write functions. It’s important to use frames before any channel operations, because the frame will serialize version information.

GSErrCode MazeSettings::Read (GS::IChannel& ic)
{
	GS::InputFrame frame (ic, classInfo);
	ic.Read (rowCount);
	ic.Read (columnCount);
	ic.Read (cellSize);
	ic.Read (createGroup);
	ic.Read (createSlab);
	return ic.GetInputStatus ();
}

GSErrCode MazeSettings::Write (GS::OChannel& oc) const
{
	GS::OutputFrame frame (oc, classInfo);
	oc.Write (rowCount);
	oc.Write (columnCount);
	oc.Write (cellSize);
	oc.Write (createGroup);
	oc.Write (createSlab);
	return oc.GetOutputStatus ();
}

Storing Data in Preferences

One last step is needed: we have to convert the data to a binary blob, and vice versa. At the end of the day we will need a pointer that points to binary representation of the class. We can achieve this by using IO::MemoryOChannel and IO::MemoryIChannel.

Writing to preferences can be done by the following code:

static bool WriteMazeSettingsToPreferences (const MazeSettings& mazeSettings)
{
	GSErrCode err = NoError;

	IO::MemoryOChannel outputChannel;
	err = mazeSettings.Write (outputChannel);
	if (err != NoError) {
		return false;
	}

	UInt64 bytes = outputChannel.GetDataSize ();
	const char* data = outputChannel.GetDestination ();

	err = ACAPI_SetPreferences (PreferencesVersion, (GSSize) bytes, data);
	if (err != NoError) {
		return false;
	}

	return true;
}

When we would like to use the data we have to read it from preferences. In this case we have to handle the situation when there are no preferences data at all. If so, we have to use the default settings for the maze.

Preferences reading can be done by the following code:

static bool LoadMazeSettingsFromPreferences (MazeSettings& mazeSettings)
{
	GSErrCode err = NoError;

	Int32 version = 0;
	GSSize bytes = 0;
	err = ACAPI_GetPreferences (&version, &bytes, nullptr);
	if (err != NoError || version == 0 || bytes == 0) {
		return false;
	}

	char* data = new char[bytes];
	err = ACAPI_GetPreferences (&version, &bytes, data);
	if (err != NoError) {
		delete[] data;
		return false;
	}

	MazeSettings tempMazeSettings;
	IO::MemoryIChannel inputChannel (data, bytes);
	err = tempMazeSettings.Read (inputChannel);
	if (err != NoError) {
		delete[] data;
		return false;
	}

	mazeSettings = tempMazeSettings;

	delete[] data;
	return true;
}

Putting All Together

Now we should access preferences data before opening the dialog, and store it when the dialog is closed. We will store it only when the user clicks OK, because usually we don’t want to modify anything on Cancel.

static bool GetMazeSettingsFromDialog (MazeSettings& mazeSettings)
{
	MazeSettings initialMazeSettings (10, 20, 1.0, true, true);
	LoadMazeSettingsFromPreferences (initialMazeSettings);

	MazeSettingsDialog mazeSettingsDialog (initialMazeSettings);
	if (mazeSettingsDialog.Invoke ()) {
		mazeSettings = mazeSettingsDialog.GetMazeSettings ();
		WriteMazeSettingsToPreferences (mazeSettings);
		return true;
	} else {
		return false;
	}
}

Summary

Writing to preferences is relatively easy, because we don’t have to deal with teamwork situations. When it comes to storing data in the plan file itself, things get a bit more complicated but of course there is a solution for this scenario as well.

I hope you enjoyed this tutorial series. If you are interested, here are some links for additional resources: