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:
- Part 1: Create elements, handle undo scope.
- Part 2: Create a dialog to manipulate the functionality.
- Part 3: Store dialog data in preferences.
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:
- Serialize your data to a binary blob.
- Write the binary data to preferences.
- Read the binary data from preferences.
- 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:
- Find out more details from Archicad API Reference.
- If you are feeling stuck, you can ask your questions in our Developer Forum.
- For some more tutorials check the Add-On Developer Blog.
- Follow us on Twitter to get the latest news.