|
Creating Compound ComponentsComponents 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:
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 ComponentsThe 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.
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.
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 SubcomponentsBeginning 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. 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.
External ComponentsWhen 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: 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;
Referring to Components with InterfacesWhen 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.
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; |
|
Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|