Mastering Delphi 7 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Mastering Delphi 7 [Electronic resources] - نسخه متنی

Marco Cantu

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
توضیحات
افزودن یادداشت جدید






Creating Compound Components

Components don't exist in isolation. Programmers often use components in conjunction with other components, coding the relationship in one or more event handlers. An alternative approach is to write compound components, which can encapsulate this relationship and make it easy to handle. There are two different types of compound components:


Internal Components Created and managed by the main component, which may surface some of their properties and events.

External Components Connected using properties. Such a compound component automates the interaction of two separate components, which can be on the same or a different form or designer.


In both cases, development follows some standard rules.

A third, less-explored alternative, involves the development of component containers, which interact with the child controls. This is a more advanced topic, and I won't explore it here.


Internal Components


The next component I will focus on is a digital clock. This example has some interesting features. First, it embeds a component (a Timer) in another component; second, it shows the live-data approach: you'll be able to see a dynamic behavior (the clock time being updated) even at design time, as it happens, for example, with data-aware components.





Note

The first feature has become more relevant since Delphi 6, because the Object Inspector now allows you to expose properties of subcomponents directly. As a result, the example presented in this section has been modified (and simplified) compared to the Delphi 5 edition of the book. I'll mention the differences, when relevant.


The digital clock will provide some text output, so I considered inheriting from the TLabel class. However, doing so would allow a user to change the label's caption—that is, the text of the clock. To avoid this problem, I used the TCustomLabel component as the parent class. A TCustomLabel object has the same capabilities as a TLabel object, but few published properties. In other words, a class that inherits from TCustomLabel can decide which properties should be available and which should remain hidden.





Note

Most of the Delphi components, particularly the Windows-based ones, have a TCustomXxx base class, which implements the entire functionality but exposes only a limited set of properties. Inheriting from these base classes is the standard way to expose only some of the properties of a component in a customized version. You cannot hide public or published properties of a base class, unless you hide them by defining a new property with the same name in the descendant class.


With past versions of Delphi, the component had to define a new property, Active, wrapping the Enabled property of the Timer. A wrapper property means that the get and set methods of this property read and write the value of the wrapped property, which belongs to an internal component (a wrapper property generally has no local data). In this specific case, the code looks like this:


function TMdClock.GetActive: Boolean;
begin
Result := FTimer.Enabled;
end;
procedure TMdClock.SetActive (Value: Boolean);
begin
FTimer.Enabled := Value;
end;


Publishing Subcomponents


Beginning with Delphi 6, you can expose the entire subcomponent (the timer) in a property of its own that will be regularly expanded by the Object Inspector, allowing a user to set each of its subproperties and even to handle its events.

Here is the full type declaration for the TMdClock component, with the subcomponent declared in the private data and exposed as a published property (in the last line):


type
TMdClock = class (TCustomLabel)
private
FTimer: TTimer;
protected
procedure UpdateClock (Sender: TObject);
public
constructor Create (AOwner: TComponent); override;
published
property Align;
property Alignment;
property Color;
property Font;
property ParentColor;
property ParentFont;
property ParentShowHint;
property PopupMenu;
property ShowHint;
property Transparent;
property Visible;
property Timer: TTimer read FTimer;
end;

The Timer property is read-only, because I don't want users to select another value for this component in the Object Inspector (or detach the component by clearing the value of this property). Developing sets of subcomponents that can be used alternately is certainly possible, but adding write support for this property in a safe way is far from trivial (considering that the users of your component might not be expert Delphi programmers). So, I suggest you stick with read-only properties for subcomponents.

To create the Timer, you must override the clock component's constructor. The Create method calls the corresponding method of the base class and creates the Timer object, installing a handler for its OnTimer event:


constructor TMdClock.Create (AOwner: TComponent);
begin
inherited Create (AOwner);
// create the internal timer object
FTimer := TTimer.Create (Self);
FTimer.Name := 'ClockTimer';
FTimer.OnTimer := UpdateClock;
FTimer.Enabled := True;
FTimer.SetSubComponent (True);
end;

The code gives the component a name for display in the Object Inspector (see Figure 9.4) and calls the specific SetSubComponent method. You don't need a destructor; the FTimer object has the TMDClock component as owner (as indicated by the parameter of its Create constructor), so it will be destroyed automatically when the clock component is destroyed.


Figure 9.4: The Object Inspector can automatically expand sub-components, showing their properties, as in the case of the Timer property of the TMdClock component.





Note

In the previous code, the call to the SetSubComponent method sets an internal flag that's saved in the ComponentStyle property. The flag (csSubComponent) affects the streaming system, allowing the subcomponent and its properties to be saved in the DFM file. The streaming system by default ignores components that are not owned by the form.


The key piece of the component's code is the UpdateClock procedure, which is just one statement:


procedure TMdLabelClock.UpdateClock (Sender: TObject);
begin
// set the current time as caption
Caption := TimeToStr (Time);
end;

This method uses Caption, which is an unpublished property, so that a user of the component cannot modify it in the Object Inspector. This statement displays the current time continuously, because the method is connected to the Timer's OnTimer event.





Note

Events of subcomponents can be edited in the Object Inspector, so that a user can handle them. If you handle the event internally, as I did in TMdLabelClock, a user can override the behavior by handling the event, in this case OnTimer. In general, the solution is to define a derived class for the internal component, overriding its virtual methods, such as the TTimer class's Timer method. In this case, though, this technique won't work, because Delphi activates the timer only if an event handler is attached to it. If you override the virtual method and do not provide the event handler (as would be correct in a subcomponent), the timer won't work.



External Components


When a component refers to an external component, it doesn't create this component itself (which is why this is called external). It is the programmer using the components that creates both of them separately (for example dragging them to a form from the Components Palette) and connects the two components using one of their properties. So we can say that a property of a component refers to an externally linked component. This property must be of a class type that inherits from TComponent.

To demonstrate, I've built a nonvisual component that can display data about a person on a label and refresh the data automatically. The component has these published properties:


type
TMdPersonalData = class(TComponent)
...
published
property FirstName: string read FFirstName write SetFirstName;
property LastName: string read FLastName write SetLastName;
property Age: Integer read FAge write SetAge;
property Description: string read GetDescription;
property OutLabel: TLabel read FLabel write SetLabel;
end;

There is some basic data plus a read-only Description property that returns all the information at once. The OutLabel property is connected with a local private field called FLabel. In the component's code, I've used this external label by means of the internal FLabel reference, as in the following:


procedure TMdPersonalData.UpdateLabel;
begin
if Assigned (FLabel) then
FLabel.Caption := Description;
end;

This UdpateLabel method is triggered every time one of the other properties changes (as you can see at design time in Figure 9.5), as shown here:


Figure 9.5: A component referencing an external label at design time


procedure TMdPersonalData.SetFirstName(const Value: string);
begin
if FFirstName <> Value then
begin
FFirstName := Value;
UpdateLabel;
end;
end;

Of course, you cannot use the label if it is not assigned; hence the need for the initial test. However, this test doesn't guarantee the label won't be used after it is destroyed (either at run time or at design time). When you write a component with a reference to an external component you need to override the Notification method in the component you are developing (the one with the external reference). This method is fired when a sibling component (one having the same owner) is created or destroyed. Consider the case of the TMdPersonalData class that receives the notification of the destruction (opRemove) of the Label component:


procedure TMdPersonalData.Notification(
AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (AComponent = FLabel) and (Operation = opRemove) then
FLabel := nil;
end;

This code is enough to avoid problems with components in the same form or designer (such as a data module), because when a component is destroyed, its owner notifies all the other components it owns (the siblings of the one being destroyed). To account for components connected across forms or data modules, however, you need to perform an extra step. Every component has an internal notification list of one or more components it must notify about its destruction. Your component can add itself to the notification list of components hooked to it (in this case, the label) by calling its FreeNotification method. So, even if the externally referenced label is on a different form, it will tell the component it is being destroyed by firing the Notification method (which is already handled and doesn't need to be updated):


procedure TMdPersonalData.SetLabel(const Value: TLabel);
begin
if FLabel <> Value then
begin
FLabel := Value;
if FLabel <> nil then
begin
UpdateLabel;
FLabel.FreeNotification (Self);
end;
end;
end;





Tip

You can also use the opposite notification (opInsert) to hook up components automatically as they are added to the same form or designer. I'm not sure why this technique is so rarely used, because it's helpful in many common situations. It is true that it makes more sense to build specific property and component editors to support design-time operations, rather than embed special code within the components.



Referring to Components with Interfaces


When referring to external components, we've traditionally been limited to a subhierarchy. For example, the component built in the previous section can refer only to objects of class TLabel or classes inheriting from TLabel, although it would make sense to also be able to output the data to other components. Delphi 6 added support for an interesting feature that has the potential to revolutionize some areas of the VCL: interface-type component references.





Note

This feature is used sparingly in Delphi 7. As it is probably too late to update Delphi's data-aware components architecture using interfaces, all we can hope is that this will be used to express other future complex relationships within the library.


If you have components supporting a given interface (even if they are not part of the same subhierarchy), you can declare a property with an interface type and assign any of those components to it. For example, suppose you have a nonvisual component attached to a control for its output, similar to what I did in the previous section. I used a traditional approach and hooked the component to a label, but you can now define an interface as follows:


type
IMdViewer = interface
['{9766860D-8E4A-4254-9843-59B98FEE6C54}']
procedure View (const str: string);
end;

A component can use this viewer interface to provide output to another control (of any type). Listing 9.2 shows how to declare a component that uses this interface to refer to an external component.

Listing 9.2: A Component that Refers to an External Component Using an Interface






type
TMdIntfTest = class(TComponent)
private
FViewer: IViewer;
FText: string;
procedure SetViewer(const Value: IViewer);
procedure SetText(const Value: string);
protected
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
published
property Viewer: IViewer read FViewer write SetViewer;
property Text: string read FText write SetText;
end;
{ TMdIntfTest }
procedure TMdIntfTest.Notification(AComponent: TComponent;
Operation: TOperation);
var
intf: IMdViewer;
begin
inherited;
if (Operation = opRemove) and
(Supports (AComponent, IMdViewer, intf)) and (intf = FViewer) then
begin
FViewer := nil;
end;
end;
procedure TMdIntfTest.SetText(const Value: string);
begin
FText := Value;
if Assigned (FViewer) then
FViewer.View(FText);
end;
procedure TMdIntfTest.SetViewer(const Value: IMdViewer);
var
iComp: IInterfaceComponentReference;
begin
if FViewer <> Value then
begin
FViewer := Value;
FViewer.View(FText);
if Supports (FViewer, IInterfaceComponentReference, iComp) then
iComp.GetComponent.FreeNotification(Self);
end;
end;











The use of an interface implies two relevant differences, compared to the traditional use of a class type to refer to an external component. First, in the Notification method, you must extract the interface from the component passed as a parameter and compare it to the interface you already hold. Second, to call the FreeNotification method, you must see whether the object passed as the parameter supports the IInterfaceComponentReference interface. This is declared in the TComponent class and provides a way to refer back to the component (GetComponent) and call its methods. Without this help you would have to add a similar method to your custom interface, because when you extract an interface from an object, there is no automatic way to refer back to the object.

Now that you have a component with an interface property, you can assign to it any component (from any portion of the VCL hierarchy) by adding the IViewer interface to it and implementing the View method. Here is an example:


type
TViewerLabel = class (TLabel, IViewer)
public
procedure View(str: String);
end;
procedure TViewerLabel.View(const str: String);
begin
Caption := str;
end;








Building Compound Components with Frames


Instead of building the compound component in code and hooking up the timer event manually, you can obtain a similar effect by using a frame. Frames make the development of compound components with custom event handlers a visual operation, and thus simpler. You can share this frame by adding it to the Repository or by creating a template using the Add to Palette command on the frame's shortcut menu.

As an alternative, you might want to share the frame by placing it in a package and registering it as a component. Technically, this technique is not difficult: You add a Register procedure to the frame's unit, add the unit to a package, and build it. The new component/frame appears in the Component Palette like any other component. When you place this component/frame on a form, you'll see its subcomponents. You cannot select these subcomponents with a mouse click in the Form Designer, but you can do so in the Object TreeView. However, any change you make to these components at design time will be lost when you run the program or save and reload the form, because the changes to those subcomponents aren't streamed, unlike what happens with standard frames you place inside a form).

If this is not what you expect, I've found a reasonable way to use frames in packages, demonstrated by the MdFramedClock component (part of the examples for this chapter on the Sybex website). The components owned by the frame are turned into subcomponents by calling the SetSubComponent method. I also exposed the internal components with properties, even though this step isn't compulsory (they can be selected in the Object TreeView). Here is the component's declaration and the code for its methods:


type
TMdFramedClock = class(TFrame)
Label1: TLabel;
Timer1: TTimer;
Bevel1: TBevel;
procedure Timer1Timer(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
published
property SubLabel: TLabel read Label1;
property SubTimer: TTimer read Timer1;
end;constructor TMdFramedClock.Create(AOwner: TComponent);
begin
inherited;
Timer1.SetSubComponent (True);
Label1.SetSubComponent (True);
end;procedure TMdFramedClock.Timer1Timer(Sender: TObject);
begin
Label1.Caption := TimeToStr (Time);
end;

In contrast to the clock component built earlier, there is no need to set up the properties of the timer, or to connect the timer event to its handler function manually, because this is done visually and saved in the DFM file of the frame. Notice also that I haven't exposed the Bevel component (I haven't called SetSubComponent on it). I did this on purpose so you can experiment with this fault behavior: try editing it at design time and see that all the changes are lost, as I mentioned earlier.

After you install this frame/component, you can use it in any application. In this case, as soon as you drop the frame on the form, the timer will begin to update the label with the current time. However, you can still handle its OnTimer event, and the Delphi IDE (recognizing that the component is in a frame) will create a method with this predefined code:


procedure TForm1.MdFramedClock1Timer1Timer(Sender: TObject);
begin
MdFramedClock1.Timer1Timer(Sender);
end;

As soon as this timer is connected (even at design time) the live clock will stop, because its original event handler is disconnected. After you compile and run the program, however, the original behavior will be restored (at least, if you don't delete the previous line); your extra custom code will be executed as well. This behavior is exactly what you expect from frames. You can find a complete demo of the use of this frame/ component in the FrameClock example.

This approach is still far from linear. It is much better than in past versions of Delphi, where frames inside packages were unusable, but it isn't worth the effort. If you work alone or with a small team, it's better to use plain frames stored in the Repository. In larger organizations or to distribute your frames to a larger audience, most people will prefer to build their components the traditional way, without trying to use frames. I hope that Borland will address more complete support for the visual development of packaged components based on frames.











/ 279