Saving and loading components

by Damon Chandler

As you know, the C++Builder IDE stores and loads a project’s design-time forms and components to and from a .DFM file. The core of this work is delegated to two special component-streaming classes: TWriter and TReader. In this article, I’ll show you how to use these classes to easily save and load components to and from a file.

Saving components to a file

The TWriter class contains numerous methods for writing a component’s properties to a stream. Here, I’ll demonstrate the TWriter::WriteComponent() method, which can be used to save all of a component’s (and its sub-components’) streamable properties, including event-type properties. Here’s a utility function that uses the WriteComponent() method to save a component to a file:

#include <memory>
#include <cassert>
int __fastcall SaveComponent(
   AnsiString filename,
   TComponent* Component
  )
{
  assert(Component != NULL);
  assert(Component->Owner != NULL);

  std::auto_ptr<TFileStream> fs(
    new TFileStream(filename, fmCreate)
    );
  std::auto_ptr<TWriter> Writer(
    new TWriter(fs.get(), 4096)
    );

  // specify the Root component
  Writer->Root = Component->Owner;
  // write the component
  Writer->WriteComponent(Component);
  // return the number of bytes wrote
  return Writer->Position;
}

The TWriter class is designed for use with a TStream-type object. Here, I’ve used a TFileStream object whose pointer is passed as the first parameter of the TWriter constructor. The second parameter of the TWriter constructor is an integer that specifies how many bytes the TWriter object should use for its internal memory buffer. The TWriter class will write first to this buffer, and then to the stream when the buffer is full.

After the TWriter object is created, all that’s left is to set its Root property and then call the WriteComponent() method. The Root property specifies the owner of the component that’s to be saved. The TWriter class will stream a component’s event-type properties only if the assigned event handlers are within Root’s __published section. Thus, to stream a component, it’s important that: (1) the component and all of its sub-components share the same Owner; and (2) all of the component’s (and sub-components’) event handlers are within Root’s __published section. For example, if you stream a TTabControl object that contains child controls, you must make sure that all of these child controls have the same Owner as the tab control; and, you must make sure that all of the controls’ events handlers are within this Owner’s __published section. The first restriction isn’t much of a problem if all of the controls are created at design time; recall that the parent form owns all design-time created controls. Keep this restriction in mind, though, when creating controls dynamically.

Loading components from a file

The TWriter class has a counterpart, TReader, that’s designed to load components from a stream. As you might have guessed, the specific method for doing this is called ReadComponent(). Here’s a utility function that demonstrates how to use the TReader::ReadComponent() method to load a component (including its sub-components) from a file:

int __fastcall LoadComponent(
   AnsiString filename,
   TComponent*& Component
  )
{
  assert(Component != NULL);
  assert(Component->Owner != NULL);

  std::auto_ptr<TFileStream> fs(
    new TFileStream(filename, fmOpenRead)
    );
  std::auto_ptr<TReader> Reader(
    new TReader(fs.get(), 4096)
    );

  // set Root, Owner, and Parent
  Reader->Root = Component->Owner;
  TControl* Control =
    dynamic_cast<TControl*>(Component);
  if (Control) {
    Reader->Parent = Control->Parent;
  }

  // remove the existing component
  delete Component; Component = NULL;

  // load the stored component
  Reader->BeginReferences();
  try {
    Component =
      Reader->ReadComponent(NULL);
  }
  __finally {
    Reader->FixupReferences();
    Reader->EndReferences();
  }

  // return the number of bytes read
  return Reader->Position;
}

Because the TWriter and TReader classes descend from the same parent class (TFiler), both classes share a common interface. The parameters of the TReader constructor, for example, are the same as those of the TWriter constructor. Similarly, the TReader class has a Root property that serves the same role as the aforementioned TWriter::Root property. Notice from this code however, that the TReader class has an additional property, Parent, that needs to be set if the component to be loaded is a TControl-type descendant. This specification is needed because the TControl::Parent property isn’t streamed.

The LoadComponent() utility function is designed for use in replacing an existing component with its stored counterpart. You specify the existing component via the Component parameter, whose value will point to the newly loaded component. Thus, after the TReader::Root and Parent properties are set, the existing component (Component) is deleted, and then its stored counterpart is loaded by using the ReadComponent() method. The call to the ReadComponent() method is preceded by a call to the BeginReferences() method and followed by calls to FixupReferences() and EndReferences(). These latter three methods ensure that all components in the stream are created before any component references are assigned.

Conclusion

There’s one caveat that you need to be aware of: The classes of all components loaded from a file must be registered with the streaming system before you call the LoadComponent() function. The code that accompanies this article (available at www.bridgespublishing.com) demonstrates how to do this, and it provides examples of how to use the SaveComponent() and LoadComponent() functions.