| |
t e m p o r a l |
d o o r w a y |
|||||||||
Creating Aggregate Components |
|||||||||||
IntroductionAs 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 ConstructionA 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:
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): Beforeobject LabelPanel: TPanel
After (In the Constructor for SampleAggregate, a Component Based On TPanel)
Owning and ParentingThe 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 SubcomponentsFor 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
Consequences Of The Life CycleWhen 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 SubcomponentsFirst, 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:
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.
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 MacrosThe 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:
The Complete ExamplesThese 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. TestingEvery aggregate component needs some tests:
CaveatsThere are some things I haven't tried with this.
ConclusionWith the addition of this technique, C++ Builder has multiple levels of inheritance support:
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
|