Mega Code Archive

 
Categories / Delphi / Examples
 

Component writing, part 1

First in a three part series covering component writing in Delphi. This article originally appeared in Delphi Developer Copyright Pinnacle Publishing, Inc. All rights reserved. This first part demonstrates some of the best approaches to building components, and at the same time provides tips on deciding on the best base class to inherit from, using virtual declarations, the process of overriding, and so on. The first two things to ask yourself are why you should make use of component writing, and when you should need to write a component. The first question is easy to answer, in fact, it has many answers. Ease of use: Encapsulated code means that you can simply drop the same component onto various forms again and again without writing a single line of code. Debugging: Centralised code makes it easy to repair an entire application (or set of applications) by fixing an error in a single component and recompiling. Money: There are a lot of companies that are only too happy to pay for the privilege of not reinventing the wheel. The second question is not too difficult to answer either. Whenever you find yourself having to write the same code more than once it is a good idea to write a component, especially if the code performs differently based on given parameters. How components are created The first step is to decide which base class you need to derive your component from. When you derive from another class you inherit property / method and event that that component owns. Below is an example of how to decide which class you should inherit from when writing your own component. (Click for full view) In the case of method A and method B, presume that the component in question is TMemo. Method Solution A Derive from TMemo B Derive from TCustomMemo C Derive from TCustomControl D Derive from TGraphicControl E Derive from TComponent This may seem a little complicated, so let me explain the process. A: When you simply need to add extra functionality to an existing component you derive from that component. This will automatically give your new component all of the existing functionality and properties of the existing component. B: Sometimes you not only need to add functionality, but you need to remove it at the same time. The standard practise when writing a component is first to write a TCustomXXXXX component where all of the properties / events and methods are declared within the Protected section of the component. The actual component is then derived from this base class. The trick therefore is not to hide functionality, but to derive your component from the "custom" version of the component and only publish the properties you want to keep (effectively removing unwanted properties / events). C: Any component that needs to receive focus needs a window handle. TWinControl is where this handle is first introduced. TCustomControl is simply a TWinControl with its own Canvas property. D: TGraphicControl does not have a window handle and therefore cannot receive focus. Components such as TLabel are derived from this base class. E: Some components are not created to enhance GUI interactivity but to make your life easier. Anything derived from TComponent is only visible at design-time. A typical use of this type of component is for database connections, timers etc. Once we have decided what our base class should be, the next step is to create the component. From the Component menu, select New Component and you will see the following dialog. Ancestor type This is the base type that we need to descend from Class name This is the class name of our new component (prefixed with the letter "T") Palette page This is which tab on the component palette you would like your component to appear on, entering a nonexistent tab will tell Delphi that you would like a new tab created. Writing some code Already it is time for us to write something. This first example will have no use at all except to demonstrate some basic concepts of component writing. First of all, select Component from the main Delphi menu and then select New Component. Enter TComponent as the "Ancestor type" and TFirstComponent as the name of your component. Next click the "Install" button. At this point you may either install your new component into an existing package (a package holds a collection of components) or install into a new package. Click "into new package" and click the "Browse" button. Once you have selected a path and filename for your new package and entered a description click "OK". At the next dialog (asking if you would like to recompile your package) click "Yes". Once the package is compiled and installed, save your package and unit. At this point we have specified our base class, and also our new class name. We have created a new package to contain our component and have been presented with a skeleton class structure by Delphi. If you look at the source code provided by Delphi you will now see the Private, Protected, Public, and Published sections. Encapsulation Encapsulation is a simple concept to grasp, but extremely important to component writing. Encapsulation is implemented with the use of four reserved words: Private, Protected, Public, and Published, you will see the Delphi has included these sections automatically in the source code for your new component. Section Accessibility Private Methods, Properties and Events declared within this section will only be accessible to the unit the component is contained within. Components in the same unit can access each other's Private items. Protected Methods, Properties and Events declared within this section will also accessible to any class descended from this class. Public Methods, Properties and Events declared here accessible from anywhere. Published This section allows you to declare properties / events that will appear in the object inspector. These settings are design time values that are saved with your project. (Not all types are supported, arrays for example are not). Starting to write our component Delphi now needs to know all about our new component. Enter the following code into the component source code. private { Private declarations } FStartTime, FStopTime :DWord; protected { Protected declarations } function GetElapsedTime: string; virtual; public { Public declarations } procedure Start; virtual; procedure Stop; virtual; property StartTime:DWord read FStartTime; property StopTime:DWord read FStopTime; property ElapsedTime: string read GetElapsedTime; published { Published declarations } end; What we have done here is added two variables FStartTime and FStopTime (it is standard to preceed variable names with the letter F). There are two methods for controlling these variables, Start and Stop. We have added a GetElapsedTime function which will return FStopTime - FStartTime as a string. Finally we have added three read-only properties. Press SHIFT-CTRL-C and Delphi will automatically complete the code for your class (or click the right mouse button and select "Complete class at cursor"). Next enter the following code for each respective method. { TFirstComponent } function TFirstComponent.GetElapsedTime: string; begin Result := IntToStr(FStopTime - FStartTime); end; procedure TFirstComponent.Start; begin FStartTime := GetTickCount; end; procedure TFirstComponent.Stop; begin FStopTime := GetTickCount; end; end. Test drive Save your unit, and reopen your package (File, Open Project from the menu, and select "Delphi Package" for the file type), once your package is open click the "Compile" button. You can also open your package by choosing Component from the main menu and then Install Packages. Select your package and then click the "Edit" button. You can now drop a TFirstComponent onto a form, in fact, you can drop as many as you like. Add two buttons (btnStart and btnStop) and add the following code to your form, and then run your test app'. procedure TForm1.btnStartClick(Sender: TObject); begin FirstComponent1.Start; end; procedure TForm1.btnStopClick(Sender: TObject); begin FirstComponent1.Stop; Caption := FirstComponent1.ElapsedTime; end; Clicking the "Start" button will mark the start time (GetTickCount is a WinAPI command that returns the number of milliseconds since Windows started). Clicking the "Stop" button will mark the stop time, and change the caption of the form. Virtual, Dynamic, Abstract and Override You may have noticed the virtual declaration after Start, Stop and GetElapsedTime. The following exercise will explain their uses. Create a new component, derive this component from TFirstComponent (name it TSecondComponent) and install it. The Virtual and Dynamic identifiers are a component writer's way of telling Delphi that the method may be replaced in a descendent class. If we Override a method in a class, our new code will be executed instead of the original code. protected { Protected declarations } function GetElapsedTime: string; override; We then implement the above code as follows. function TSecondComponent.GetElapsedTime: string; var S: string; begin S := inherited GetElapsedTime; Result := S + ' milliseconds or ' + Format('%.2f seconds', [(StopTime - StartTime) / 1000]); end; Our new code is now called in replacement of the original GetElapsedTime, even calls implemented in TFirstComponent to GetElapsed time will now call our new code (if we have created an instance of TSecondComponent that is). The original code is invoked through the use of the Inherited command. Note : If you do not "override" a base method (because the base was not declared as virtual or because you forgot). TSecondComponent will call your new code, whereas any code implemented in TFirstComponent will still continue to call the original code from TFirstComponent. The Abstract identifier tells Delphi not to expect any code for the named method. You should not create an instance of any object with abstract methods in them (such as TStrings). The standard practise is to create a descendant of such a class and to override all abstract methods (such as TStringList does). Dynamic Vs Virtual is simply a question of speed Vs size. A Dynamic method will result in each instance of a class requiring less memory, whereas a Virtual method will execute faster at the cost of a little extra memory. There are a few simple steps to adding events to your component. Events allow the component to communicate with your application, to notify it when something important has happened. An event is merely a read / write property, instead of being a simple variable type (such as string, integer etc) it is a procedure or function. Create a new component, descend it from TSecondComponent and name it TThirdComponent. Save the unit, install your component, and add the following code. type TState = (stStarted, stStopped); TStateChangeEvent = procedure Sender : TObject; State : TState) of object; TThirdComponent = class(TSecondComponent) private { Private declarations } FState: TState; FOnStart, FOnStop: TNotifyEvent; FOnStateChange: TStateChangeEvent; protected { Protected declarations } public { Public declarations } constructor Create(AOwner : TComponent); override; destructor Destroy; override; procedure Start; override; procedure Stop; override; property State: TState read FState; published { Published declarations } property OnStart: TNotifyEvent read FOnStart write FOnStart; property OnStateChange: TStateChangeEvent read FOnStateChange write FOnStateChange; property OnStop: TNotifyEvent read FOnStop write FOnStop; end; Events are simply procedures or functions (rarely) that belong to a class (hence the "of object" clause you see in the TStateChangeEvent). For example, TNotifyEvent is a standard event type implemented by Delphi which just passes the object that triggered the event, it is always good to send "Self" (Sender : TObject) as the first parameter of any event as the same event code may be used for multiple components. TNotifyEvent is defined as type TNotifyEvent = procedure (Sender: TObject) of object; To call an event from within a component is just a case of checking if the event has been assigned and, if so, calling it. I have overridden the Start and Stop methods of TSecondComponent in order to trigger these events, like so. procedure TThirdComponent.Start; begin inherited; //This calls TSecondComponent.Start FState := stStarted; if Assigned(OnStart) then OnStart(Self); if Assigned(OnStateChange) then OnStateChange(Self, State); end; procedure TThirdComponent.Stop; begin inherited; //This calls TSecondComponent.Stop FState := stStopped; if Assigned(OnStop) then OnStop(Self); if Assigned(OnStateChange) then OnStateChange(Self, State); end; constructor TThirdComponent.Create(AOwner: TComponent); begin inherited; //This is were you initialise properties, and create //and objects your component may use internally FState := stStopped; end; destructor TThirdComponent.Destroy; begin //This is where you would destroy //any created objects inherited; end; Recompile your package (don't forget to save your package anytime you add a new component). Upon dropping your new component on the form you will notice that there are three events. OnStart, OnStop, and OnStateChange. If you look at Demo3 you will see how I have used these events. OnStart sets the caption to "Started" OnStop shows the elapsed time OnStateChange enables / disables the relevant Start / Stop button procedure TForm1.ThirdComponent1Start(Sender: TObject); begin Caption := 'Start'; end; procedure TForm1.ThirdComponent1Stop(Sender: TObject); begin Caption := ThirdComponent1.ElapsedTime; end; procedure TForm1.ThirdComponent1StateChange(Sender: TObject; State: TState); begin btnStart.Enabled := ThirdComponent1.State = stStopped; btnStop.Enabled := ThirdComponent1.State = stStarted; end; Standards in component writing Finally we will cover a few more points about component writing, including some existing methods of base components, and standards for writing. Creating and destroying your component: Objects are created through a constructor and destroyed through a destructor. The purpose of overriding a constructor is threefold To create any objects that it contains within itself (sub objects) To initialise values of the class (properties etc) To raise an exception and stop the class from being created. It is standard to call the inherited constructor from within your own constructor so that the parent-class can perform its initialisations, although it is not necessary to do so in order to create your component. (Your component is created as soon as your constructor is finished, it is not created by calling the inherited constructor) The purpose of overriding a destructor is simply to free any resources that were allocated during the life of component. Call inherited only after you have freed these resources. Standard component parts: Paint: You can override this method to provide your own custom drawing of your component. Loaded: This is called by Delphi as soon as all of its properties have finished being set when its parent form is created. You can override this method in order to perform any actions that depend on a group of properties all being set. Invalidate: Whenever a property is changed that affects the visual appearance of a component you should call this method. ComponentState: This property is very useful when you need to check if your component currently exists at design/run time, or if its properties are currently being read by a streaming process. Encapsulating your component properly It is standard to write your component as TCustomMyClass and then derive your component from that base class. The "custom" component you write will have most (if not all) of its properties / methods declared within its Protected section. When you descend from your "custom" class you simply redeclare your properties within the Public or Published sections. type TCustomMyClass = class(TComponent) private FSomeString: string; protected procedure SetSomeString(const Value : string); virtual; property SomeString: string read FSomeString write SetSomeString; end; TMyClass = class(TCustomMyClass) published property SomeString; end; This is good practise as it allows other people to derive their own components based on yours while still allowing them to remove certain properties. Note how SetSomeString has been declared as virtual within the protected area. This is also good etiquette as it allows descended classes to respond to changes in property values by overriding the procedure that sets them. This also applies to events, where you see an OnStateChange event you will often find a DoStateChange method, for example: type TCustomMyClass = class(TComponent) private FOnStateChange: TStateChangeEvent; protected procedure DoStateChange(State : TState); virtual; published property OnStateChange: TStateChangeEvent read FOnStateChange write FOnStateChange; end; procedure TCustomMyClass.DoStateChange(State : TState); begin if Assigned(OnStateChange) then OnStateChange(Self, State); end; Instead of writing the "If assigned(OnStateChange) then" code every time the state changes, you would simply call "DoStateChange(NewState)". Apart from being smaller to write, it also allows descendent classes to override DoStateChange and trigger necessary code in response to an event. Summary In this first article we have seen the uses of component writing. We have also seen how to write our components, and what original class to base our components on. Furthermore we discussed Virtual / Dynamic methods, and how to use them in order to implement "component etiquette". In the second part of this article we will learn how to write custom properties, such as binary data, collections, sets, and expandable sub-properties.