t e m p o r a l 
 d o o r w a y 

Creating Aggregate Components

 

Introduction

As you develop applications in C++ Builder you will probably find certain user interface patterns recurring again and again - the same control types in the same arrangements with the similar behaviors between controls. These represent important opportunities for abstraction and leverage in the development process. The same holds true for non-visual components, such as data components.

C++ Builder 1.0 does not provide facilities for directly creating such components, and C++uilder 3.0 only allows you to create "template" components, whose source is disconnected from the components created. C++Builder 5 allows you to create "frames", which like forms, can be constructed and used visually. However, all levels of frames, starting at the base class, must be included in your project if you want to use a frame that inherits from a frame inheritance tree. This can be annoying. In addition, frames have problems using anchors for user interface component layout.

The techniques in this article work under any version of C++ Builder, and provide a way to create aggregate components, and also to allow the sub-components of your component stream, and may even allow you to open the subcomponents for editing by the user so that your aggregate component can be reshaped visually and by properties to meet the needs of other applications or other programmers.

Basic Aggregate Component Construction

A basic aggregate component typically is derived from TWinControl. Its constructor generates a number of child components (subcomponents) at design and at run time. Like any other component, it exposes properties and events to the developer. Some of those influence the component, others influence the child components. When the component is streamed, the published properties of the component itself are saved and the child components are kept invisible from the streaming system.

Creating such a component is fairly simple, and you can leverage off the text representation, available when you copy a set of components to the clipboard, to make your life easier:

  1. Lay out the component on a form and select the resulting controls, or pick the controls from some existing application form. Edit | Copy. You now have a representation of the properties of these controls on the clipboard.

  2. Component | New; derive the component from TWinControl.

  3. Save the component to the appropriate location.

  4. Click inside the component constructor body and Edit | Paste. This pastes in the text representation of the controls from your source.

  5. Click at the top of the constructor body. Edit | Replace "object " with "", ": " with " = new ", "." with "->" , "end" with "" and single quotes with double quotes. This takes care of all of the simple conversions from the Pascal-like text to C++.

  6. Add semi-colons to the end of all of the lines.

  7. Add the owner parameter to the component creation lines. (i.e. EditControl = new TEdit(Owner)).

  8. For each property of each subcomponent, put subcomponentname-> at the beginning.

  9. For each subcomponent, add a line to set the Parent of the subcomponent to "this".

  10. For each subcomponent, add a line to set the name of the subcomponent to the same name as its variable.

  11. Copy the event handlers (if any) from the source form to the component .cpp file. Change the class name on each handler to the name of the component class. Note that the text you pasted earlier already references those handlers.

  12. Add a declaration for each subcomponent to the header file.

  13. Add any desired properties or events to the __published section of the component header file.

  14. Install the component with Component | Install. Deal with the inevitable compiler errors. Your aggregate component is ready for testing.

  15. Determine the optimal size for the aggregate - by default it will be 0 width and height. Once you have it, put that property setting in the constructor and rebuild the library.

Once completed, this aggregate component is no different from any other component. Note that the above, however, does not deal with any additional mechanism you may want to provide to allow flexible sizing of the component or resizing of subcomponents to maintain their relationships with the component.

Pascal to C++

Here is a before and after of an aggregate component initialization created from copying a set of components to the clipboard and performing the edits outlined above (it includes an OCX as well as normal VCL components):

Before

object LabelPanel: TPanel
   Left = 0
   Top = 0
   Width = 328
   Height = 33
   Align = alTop
   Color = clTeal
   TabOrder = 0
   objectLabel:TLabel
      Left = 8
      Top = 8
      Width = 34
      Height = 16
      Caption = 'Label'
      Font.Charset = DEFAULT_CHARSET
      Font.Color = clWhite
      Font.Height = -13
      Font.Name = 'Arial'
      Font.Style = [fsBold,fsItalic]
      ParentFont = False
   end
end
object Grid: TDBGrid
   Left = 0
   Top = 33
   Width = 328
   Height = 315
   TitleColor = clBtnFace
   FixedCols = 0
   ShowHorzScrollBar = True
   Align = alClient
   Font.Charset = DEFAULT_CHARSET
   Font.Color = clWindowText
   Font.Height = -13
   Font.Name = 'Arial'
   Font.Style = []
   ParentFont = False
   TabOrder = 1
end

After (In the Constructor for SampleAggregate, a Component Based On TPanel)

LabelPanel= new TPanel(this);
   LabelPanel->Left = 0;
   LabelPanel->Top = 0;
   LabelPanel->Width = 328;
   LabelPanel->Height = 33;
   LabelPanel->Align = alTop;
   LabelPanel->Color = clTeal;
   LabelPanel->TabOrder = 0;
   LabelPanel->Name = "LabelPanel"; // You have to add this


   Label = new TLabel(LabelPanel);
      Label->Parent = LabelPanel;
      Label->Left = 8;
      Label->Top = 8;
      Label->Width = 34;
      Label->Height = 16;
      Label->Caption = 'Label';
      Label->Font->Charset = DEFAULT_CHARSET;
      Label->Font->Color = clWhite;
      Label->Font->Height = -13;
      Label->Font->Name = 'Arial';
      Label->Label->Font->Style = [fsBold,fsItalic];
      Label->ParentFont = False;
      Label->Name = "Label";


Grid = new DBGrid(this);
   Grid->Parent = this;
   Grid->Left = 0;
   Grid->Top = 33;
   Grid->Width = 328;
   Grid->Height = 315;
   Grid->TitleColor = clBtnFace;
   Grid->FixedCols = 0;
   Grid->ShowHorzScrollBar = True;
   Grid->Align = alClient;
   Grid->Font->Charset = DEFAULT_CHARSET;
   Grid->Font->Color = clWindowText;
   Grid->Font->Height = -13;
   Grid->Font->Name = 'Arial';
   Grid->Font->Style = [];
   Grid->ParentFont = False;
   Grid->TabOrder = 1;

   Grid->Name = "Grid";

Owning and Parenting

The aggregate component's subcomponents are both owned and parented by the aggregate. This means that you do not have access to subcomponents from the IDE at design time, except through properties exposed by the component, or through a component editor.

This may seem inflexible, but it means that you do not have to worry about the complexities of allowing subcomponents to be streamed, and subcomponents cannot accidentally be deleted at design time.

Streaming Subcomponents

For whatever reason, you may want to be able to stream subcomponents. Perhaps you allow the programmer to reposition them by direct manipulation at design time, or perhaps you expose their component editors, or have some other changes you want to apply that can't really be represented as exposed properties of the aggregate component.

But streaming subcomponents is not as simple as you might think from the documentation. According to the documentation, for instance, child components are always streamed. That may be true for some specialized classes like TPanel, but in those classes, the child components are owned by the form, not the panel, even though the panel parents them. That is not a simple relationship to establish.

A second reading of the documentation and some references might make it seem as simple as providing an override to TComponent::GetChildren, which is called by the streaming system to determine what the child components of your component might be. That is also not true, for reasons which relate to the stages in the life of an aggregate component instance...

Stages In The Life Of An Aggregate Component Instance

  • Being dropped on the form for the first time - the new instance must create all of the subcomponents for the first time.
  • Being saved - the new instance must stream its subcomponents.
  • Being loaded - the instance must allow the streaming process to create the subcomponents.

Consequences Of The Life Cycle

When the component is dropped on the form for the first time, it has no way of knowing that this is the first time. It cannot tell whether the subcomponents will be created by the stream facility. So it must create the subcomponents in its constructor, to be safe. This is especially important if any properties of the subcomponents are set through properties of the aggregate, since the subcomponents will be updated by the streaming system through those properties.

But it must also delete subcomponents created in the constructor before they are created by the stream.

In addition, it must ensure that components brought in by the streaming system have their owner set properly, since the streaming system does not track ownership, and defaults to everything being owned by the form., which essentially disaggregates the aggregate component.

Implementing Streaming Subcomponents

First, create a standard aggregate component as described above, except that the owner for all subcomponents should be the Owner of the aggregate, not this (the owner of the aggregate usually turns out to be the form, though that is not explicit in the code for the constructor).

Then implement the GetChildren method:

void __fastcall GetChildren(Classes::TGetChildProc Proc)
{
   for (int Index = 0; Index < ControlCount; Index++)
   {
      Proc(Controls[Index]);
   };
};

This tells the streaming system to stream all of the subcomponents.

Next, override the method that handles reading the subcomponent stream. This method registers the classes of the subcomponents, which is required for the incoming stream to create instances of those classes (failure to do this causes EClassNotFound exceptions at run time, even when everything seems fine at design time). It then destroys the components created by the constructor. Finally, it calls the superclass version of itself, which commences the process of streaming in the subcomponents. Note that this is the second most error-prone of the methods you have to implement, since it is easy to forget to register a subcomponent class.

void __fastcall SampleAggregate::ReadState(Classes::TReader* Reader)
{
   DestroyComponents(); // Get rid of constructor components

   TComponentClass StreamUsedClasses[3] =
   {
      __classid(TDBGrid),
      __classid(TLabel),
      __classid(TPanel)
   };

   RegisterClasses(StreamUsedClasses,2); // Index is high position not count
   TWinControl::ReadState(Reader);
};

What, you may ask, is "RegisterClasses"? Well, it's simple. C++ is a compiled language, and knowledge of every class you intend to create must be present in the executable when it is run. RegisterClasses forces you to reference class definitions in your code so that those definitions will be included in the program executable, and when executed at run time, it notifies the run-time type information facilities (RTTI) of the presence of these classes and that they will be used.

The last method you need to override is the Loaded method, which is invoked after the completion of the streaming process and once all of the intercomponent references between the subcomponents have been "fixed up". This is the most error-prone of the methods you have to override, since it is really easy to forget to look for a named subcomponent and reassign it to its variable, or to reestablish a subcomponent's event handlers.

void __fastcall SampleAggregate::Loaded(void)
{
   for (int Index = 0; Index < ControlCount; Index++)
   {
      if (Controls[Index]->Name == "LabelPanel") LabelPanel = (TPanel *) Controls[Index];
      else if (Controls[Index]->Name == "Label") Label = (TLabel *) Controls[Index];
      else if (Controls[Index]->Name == "Grid") Grid = (TDBGrid *) Controls[Index];
   };

   // Reassign any event handlers used internally

   Grid->OnCellClick = PickGridEntry;
}

This method goes through all of the loaded components and determines which ones should be assigned to which internal variables, based on the component name. It also reestablishes the event handlers, since, for some reason, the outgoing streaming system seems to drop them.

Nested Subcomponents And Helper Macros

The Loaded method needs to be altered if the subcomponents are nested in each other - that is if they have have as parent a child of the aggregate. The following two macros help in dealing with the complexity of recasting streamed in components to internal variables and in properly reparenting when there are nested subcomponents:

#define Recast(ControlName,Type) if (Control->Name == #ControlName) ControlName = (Type) Control

#define Reparent(ControlName,ControlParent) if (Control->Name == #ControlName) Parent = ControlParent

They can be used as follows:

Recast(ControlPanel,TPanel *);
   else Recast(CrossHairHorizontal,PVWindowRect *);
   else Recast(CrossHairVertical,PVWindowRect *);

Reparent(ControlPanel,this);
   else Reparent(CrossHairVertical,this);
   else Reparent(CrossHairHorizontal,this);

The Complete Examples

These are the header and .cpp files for three example aggregate components. A .zip file containing the source for these components and a test application can be downloaded. Use at your own risk - I am not responsible for anything this zip file or the contained programs may do to your system (though they have been checked carefully for safety and the absence of viruses). Each component is commented to show how it does what it does, and how it differs from or extends the techniques discussed above.

1) A simple aggregate that does not stream its internal subcomponents.

2) A simple aggregate that streams its internal subcomponents, but does not expose them for object inspector editing. This can be used for aggregates that use a component editor only to change the properties of the subcomponents.

3) A simple aggregate that allows the programmer to change any of the properties of its subcomponents at design time, and which streams the changes so that they will still be in effect the next time the application that uses it is opened.

To use the examples, create a directory to contain them, download the zip file to that directory, and then use Winzip or some other unzip utility to extract them to that same directory. There are two project groups: Application.bpg and ProjectGroup.bpg. Open ProjectGroup first - it contains the component package. In the Project Manager, right button on this project group and pick "Install". That should successfully add the components to your Component Palette. Then you can safely open Application.bpg, which contains a form showing an instance of each of the three components.

Testing

Every aggregate component needs some tests:

  • Drop it on the form. Use that to determine that the constructor creates the subcomponent instances, and that the subcomponents are properly parented and owned (you can't drag subcomponents at design time if you've done it right).

  • Click on the component, Edit | Copy, and paste the result into notepad. Inspect it to make sure the subcomponents are streamed if that's what you want, and that parent child relationships are maintained, and that properties have the appropriate values. Note that the event handler assignments WILL BE MISSING.

  • Save the form with the component, Close All, and Reopen. If everything goes OK, and the component looks normal, so far so good. Move the component to make sure that you can't move subcomponents, and to make sure subcomponents haven't been duplicated (a sure sign that the form is the owner (you didn't override GetChildOwner)).

  • Change the component, Save All, Close All, and Reopen. If the change takes, streaming is working fine.

  • Run the project. If everything compiles and the project runs, and all of the changes are OK, you're set. Exercise the component to make sure all of the event handlers have been restored after streaming. If you get an EClassNotFound, you forgot to register a subcomponent class in ReadState with RegisterClasses. If you get access violations, you forgot to reassign a subcomponent to the internal variable it needs to occupy.

Caveats

There are some things I haven't tried with this.

  • Deriving an aggregate component from another aggregate component.

  • Deriving a form containing an aggregate component from the Object Repository.

  • One thing I would like to try is having a component editor that changes the owner of the subcomponents to the form, lets you move them around, and then let's you freeze them as owned by the aggregate to be saved. More on that later.

Conclusion

With the addition of this technique, C++ Builder has multiple levels of inheritance support:

  • You can inherit combinations of forms and data modules from the repository (Application Inheritance).

  • You can aggregate patterns of controls into components which can themselves be the basis for inheritance, and which can be used in applications which are inherited (this document).

  • You can create components derived from existing components, adding or modifying behavior and appearance (the normal C++ Builder way, as described in the documentation and most books).

Aggregate components allow for powerful types of encapsulation. For instance, you can abstract user interface patterns. Or you can combine data components with their user interfaces, and you can provide as much abstraction in the source of the data that populates the user interface as you desire - and since everything is encapsulated, it is powerfully reusable.

That, after all, is the dream. Drop it on a form and tweak a few properties, and run it.

Copyright © 2004 by Mark Cashman (unless otherwise indicated), All Rights Reserved