home   Java Script   MS Access   Perl   HTML   Delphi   C ++   Visual Basic   Java   CGIPerl   MS Excel   Front Page 98   Windows 98   Ms Word   Builder   PHP   Assembler     Link to us   Links    



Teach Yourself Borland Delphi 4 in 21 Days

Previous chapterNext chapterContents


- 20 -

Creating Components


Delphi provides a wide assortment of components to use in your applications. These components cover the basic Windows controls as well as provide some specialty components not inherent in Windows itself. Still, you might need to create a component of your own to perform a task not provided by the pre-installed components. Writing a component requires these steps:

1. Use the New Component dialog box to begin the creation process.

2. Add properties, methods, and events to the component's class.

3. Test the component.

4. Add the component to the Component palette.

Today you learn how to create components. As with most of the more difficult concepts in Delphi, it's not so bad after you've done it once or twice. You will learn this technique by building a component called TFlashingLabel. TFlashingLabel is a regular Label component that flashes its text. By the time the day is done, you will know what you need in order to create basic components.

Creating a New Component

Writing components requires a higher level of programming expertise than you have used up to this point. First, you have to create a class for your new component. The class needs to be designed so that some of its properties show up in the Object Inspector, whereas others will be used only at runtime. In addition, you will almost certainly have to write methods for your component. Some will be private to the component; others will be made public so that users of the component can access them. Finally, you might have to write events for the component. Obviously, there is some work involved. As great as Delphi's visual programming environment is, it won't help you here. Writing components is pure programming.

The New Component Dialog Box

The New Component dialog box gives you a head start on writing a component. To access the New Component dialog box, choose File|New to display the Object Repository and then double-click the Component icon. Figure 20.1 shows the New Component dialog box you see when creating a new component.

FIGURE 20.1. The New Component dialog box.

The Ancestor type field is used to specify an ancestor class for the new component. The classes of all installed components are listed in the Ancestor type combo box. When you create a new component, you should choose a base class that most closely matches the type of component you want to create.

For example, the FlashingLabel component is just a label that flashes. In that case, the standard Label component has everything you need to get started, so you could use a TCustomLabel for the ancestor class. If, on the other hand, you want to create a component that creates Windows shortcuts, you would probably derive the component from TComponent (the base class for all components) because there is no other VCL component that gives you a better base from which to work.


NOTE: Delphi provides several classes that you can use as base classes for new components. The names of these classes all start with TCustom. For example, the base class for a TLabel is TCustomLabel. You can derive from one of these classes whenever you create a new component. The custom classes already provide the properties you are most likely to need for that component type, but the properties are not published (published properties are the properties shown in the Object Inspector at design time). All you have to do to make the properties published is re-declare the base class's properties in the published section of your component's class declaration. This is important because after a property is published, it can't be unpublished. Starting with a custom class enables you to choose exactly the properties you want published.

When you derive a new component from an existing component, you are using the Object Pascal feature called inheritance. It's been a while since I talked about inheritance, so refer to Day 3, "Classes and Object-Oriented Programming," if you need a refresher course. Inheriting from a component is, in effect, taking everything that component has and adding some functionality of your own. The class you are inheriting from is the base class, and the new class is the derived class. In the previous example, TCustomLabel would be the base class and TFlashingLabel would be the derived class.

After you select an ancestor class, type the name of your new component's class in the Class Name field. The classname should begin with a T and should describe the function the class performs. The component you build today will have the name TFlashingLabel (you'll begin creating the component soon). If you choose a classname that already exists in the component library, the New Component dialog box will tell you so when you click the OK button. You will have to choose a unique classname before you can continue.


NOTE: There's no reason that you have to begin the classname with T; it just happens to be customary for Borland classes. (The use of T in Borland classes is a Borland tradition that goes back to the early days of Turbo Pascal. It was used in Turbo Vision, OWL, and now in VCL.) Some people use T when deriving from a Borland class but not when creating their own classes. It's entirely up to you.




NOTE: Professional component writers learned long ago to be sure that their component classes have unique names. Imagine the problems if two component vendors both name a component TFancyLabel. At TurboPower where I work, our Async Professional components start with TApd, our Orpheus components start with TOr, Abbrevia components start with TAb, and so on. Although this doesn't guarantee that these component names won't clash with those of other vendors, it certainly goes a long way to prevent that from happening.

The Palette Page field enables you to specify the page on the Component palette where you want the component's icon to appear. (The component's icon won't actually appear on the Component palette until you install the design package containing the component.) You can choose an existing tab on the Component palette, or you can type the name of a new tab you want Delphi to create for this component.

The Unit file name field is used to specify the name of the file that will contain the component's source. Delphi automatically creates a unit filename based on the component's name, but you can change the supplied filename if you want. The Search path field is used to specify the search path that Delphi should use when looking for component packages. You won't usually need to modify this field.

The Install button is used to install the new component directly into a package. You need not worry about that now, though, because you will use the default package that Delphi provides for miscellaneous components.

Creating the FlashingLabel Component

You are now ready to perform the first steps in creating the TFlashingLabel component. As I said earlier, this component is a regular Label component that flashes its text on the screen. With that in mind, let's get started creating the component:

1. Choose File|New to invoke the Object Repository.

2. Double-click the Component icon in the Object Repository. The New Component dialog box is displayed.

3. From the Ancestor type combo box, choose the TCustomLabel class as the base class.

4. Type TFlashingLabel in the Class Name field.

5. The Palette Page field contains Samples. Leave this field as is. The new component will be added to the Samples page of the Component palette when you install the component.

6. Click OK to close the New Component dialog box. The Code Editor appears and displays a new source code unit.

7. Save the unit as FlashingLabel.pas.

Listing 20.1 shows the source unit as it appears now.

LISTING 20.1. FlashingLabel.pas.

unit FlashingLabel;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms, Dialogs, StdCtrls;
type
  TFlashingLabel = class(TCustomLabel)
  private
    { Private declarations } 
  protected
    { Protected declarations } 
  public
    { Public declarations } 
  published
    { Published declarations } 
  end;
procedure Register;
implementation
procedure Register;
begin
  RegisterComponents(`Samples', [TFlashingLabel]);
end;
end.

As you can see, the TFlashingLabel class is derived from TCustomLabel. The class declaration is empty except for the access keywords (private, public, protected, and published). You can fill in the blanks later after I've had a chance to go over what comprises a component.


As you can see, the New Component dialog box gives you a head start by filling out some of the basic parts of the component's unit for you. You still have to do the hard stuff, but at least the Register procedure is written and the class declaration is started. Let me sidetrack here and talk about the Register procedure.

The Register Procedure

Registering the component is necessary so that Delphi knows what components are in the component library and on what tab they should appear. A typical Register procedure looks like this:

procedure Register;
begin
  RegisterComponents(`Samples', [TMyComponent]);
end;

The Register procedure calls RegisterComponents to register the component. RegisterComponents takes two parameters. The first parameter is the name of the Component palette page on which the component will appear after it is installed; the second parameter is an array of components to be registered. If you were creating an entire library of components, you could register them all at one time by placing each component's classname in the array. For example:

procedure Register;
begin
  RegisterComponents(`Essentials 1', 
    [TEsLabel, TEsScrollingMarquee, TEsCalendar,
     TEsCalculator, TEsDateEdit, TEsNumberEdit, TEsMenuButton,
     TEsColorComboBox, TEsTile, TEsGradient, TEsRollUp]);
end;

This Register procedure registers 11 components and places them on the Component palette tab called "Essentials 1." Most of the time you will be dealing with one component at a time, but you can certainly register more than one component if you need to.


NOTE: The Register procedure is also used to register component editors and property editors. Component and property editors are special editors, usually dialog boxes, that aid in modifying a component or a component's property at design time. I won't discuss component and property editors in this book because they are beyond the book's scope.

At this point the component doesn't do anything special, but before you go on with the component's creation, I need to explain what makes up a component. After that, you'll write the rest of the TFlashingLabel component. Keep the component that you just created (such as it is) open in the IDE because you'll need it a little later.

Component Properties and Methods

A big part of writing components is writing the component's properties and methods. Events are also a big part of writing components, but let's talk first about properties and methods, and then I'll discuss events.

Properties

You have been using properties a lot in your journey thus far; from a user's perspective you know what properties are. Now you need to understand properties from a component writer's perspective. Before you write components, you need to understand what properties are--and what they are not.

Specifically, properties are not class data fields. It is natural to think of properties as data fields of the class to which they belong. After all, you treat properties just like class data fields when you perform actions like this:

var

  W : Integer;
begin
  W := Width;
  Height := W * 2;

But properties are not class data fields, and you must keep that in mind when writing components. Properties differ from class data fields in many ways but have at least one feature in common with data fields: They have a specific data type. A property's type can be one of the integral data types (Integer, Word, Double, string, and so on), a class (TCanvas, TFont, and so on), or a record (TRect, for example).

What properties are, then, is a special type of object that meets the following criteria:

For this to make more sense, let's take a look at these features of properties one at a time.

Properties Have Underlying Class Data Fields

Each property has an underlying class data field associated with it. It is this data field that holds the actual value associated with a property. Take a simple assignment, for example:

Label.Caption := `Pat McGarry';

This statement assigns a string to the Caption property of a Label component. What happens behind the scenes is more than just a simple assignment, though. Because a Caption property is of the string type, it has a string object as its underlying data field. When an assignment is made like this one, the underlying data field is given the value of the assigned string. Using an underlying data field is necessary because a property does not have the capability to store data on its own.

You can name the underlying data field anything you want, but tradition dictates that the data field associated with a property have the same name as the property, with the addition of a leading F. For example, the data field associated with the Caption property is named FCaption.


NOTE: This association between the property and its underlying data field can be the source of much confusion when you start writing components. It's not that it's difficult to understand; it's just that you might mix up the two when writing code for the component. For example, you might accidentally write
Left := 20;
when you mean to write
FLeft := 20;



This results in all sorts of interesting behavior in your component. You'll see why when I discuss write methods in the next section.


The underlying data field is almost always declared as private. This is because you want your users to modify the data field through the property or by calling a method, but never directly. This way you are in control of the data field as much as possible, which leads you to the next feature of properties.

Properties Can Have Write Methods

When you make an assignment to a property, many things can happen behind the scenes. Exactly what happens depends on the specific property. For example, this code looks simple:

Left := 20;

But several things happen when this line is executed. First, the underlying data field, FLeft, is changed to the new value. Next, the form (assuming this code was executed from a form) is moved to the new left position using the Windows API function MoveWindow. Finally, the form is repainted by calling Invalidate for the form.

How does all that happen? It happens through the Left property's write method. The write method is a method that is called any time the property is assigned a value. You can use the write method to do validation of the assigned value or to 
perform special processing.

You declare a property's write method when you declare the property. Here's an example of a typical property declaration:

property FlashRate : Integer
  read FFlashRate write SetFlashRate;

This declaration contains syntax that you haven't seen before because it is syntax specific to properties. First of all, notice that the property is declared with the property keyword and that the property type follows the property name. The second line of code tells the compiler that the property is read directly from the FFlashRate data field (I'll talk more about read methods in just a bit), and that the property uses a write method called SetFlashRate. You can name the write method anything you want, but traditionally the write method has the same name as the property prepended with the word Set.

When the property is written to (assigned a value), the write method associated with the property will be called automatically. The write method must be a procedure and must have one parameter. That parameter must be of the same type as the property itself. For example, the write method's declaration for the FlashRate property would be

procedure SetFlashRate(AFlashRate : Integer);

The value passed to the write method is the value that was assigned to the property. So, given this line:

FlashingLabel.FlashRate := 1000;

You end up with the value of 1000 passed to the SetFlashRate function. What you do with the value within the write method depends on a wide variety of factors. At a minimum, you assign the value to the associated data field. In other words, the write method would look like this:

procedure TFlashingLabel.SetFlashRate(AFlashRate : Integer);

begin
  FFlashRate := AFlashRate;
  { Do some other stuff. } 
end;


NOTE: The use of the leading A for the parameter name in a write method is another Delphi tradition.

You will nearly always do something more than assign the value to the underlying data field. If you are only assigning the value to the data field associated with the property, you don't need a write method for the property. I'll explain that in just a moment. First, let's look at read methods.

Properties Can Have Read Methods

The read method works exactly like the write method (aside from the obvious difference). When a property's value is read, the read method is executed and the value of the underlying data field is returned.

The name of the read method is the property name preceded by Get. The read method takes no parameters and returns the property type. For example, if you were to use a read method for the FlashRate property, it would be declared like this:

function GetFlashRate : Integer;
The read method might perform some processing and then return the value of the underlying data field, FFlashRate in this case. The reading of a property happens in many different ways. Sometimes it's the result of an assignment to a variable:
Rate := FlashingLabel.FlashRate;

At other times it is used within a statement:

case FlashingLabel.FlashRate of
  1000 : SpeedUp;
  { etc. } 
end;

Regardless of how the property is read, the read method is called each time a read takes place.

Notice that a couple of paragraphs ago I said if you are using a read method. Frequently you won't use a read method but instead will use direct access to retrieve the value of the data field associated with a property. Let's take a look at direct access right now.

Properties Can Use Direct Access

You don't have to use read and write methods for your properties. If you are assigning a value to the underlying data field or reading the value of the underlying data field, you can use direct access. The declaration for a property using direct access looks like this:

property FlashRate : Integer

  read FFlashRate write FFlashRate;

This snippet tells the compiler that the data field itself (FFlashRate) is used for both the read and the write specifiers rather than a read method or a write method. When the property is written to, the data field is changed and nothing more takes place. When the property is read, the value of the underlying data field is returned. It's as simple as that.


NOTE: It is common to use direct access when reading the property and to use a write method for writing to the property. Take a look at this earlier example of a property declaration:
property FlashRate : Integer
  read FFlashRate write SetFlashRate;


The property uses direct access for reading, but it uses a write method for writing. Writing to a property often produces side effects, as I explained in the previous section. In fact, the capability to spawn side effects is one of the big strengths of properties. To cause side effects when your property is written to, you use a write method. Reading a property, on the other hand, is usually just a matter of returning the value of the underlying data field. In that case, direct access makes the most sense.

Properties Can Be Read-Only or Write-Only

You can specify a property to be read-only or write-only. Making a property read-only is a useful feature (VCL has many read-only properties). For example, you might have a property that you want the user to be able to read but not modify. It could be that modifying the property directly would have adverse effects on the component, so you need to protect against that.

Making a property read-only is easy, you simply omit the write specifier in the property's declaration:

property FlashRate : Integer
  read FFlashRate;

If the user attempts to write to a property that is declared as read-only, he or she will get a compiler error that says "Cannot assign to a read-only property". As you can see, making a property read-only is very simple.

You can make a property write-only by omitting the read specifier from the property's declaration. It's difficult to imagine a use for a property that can be written to but not read, but you certainly can write such a property, if necessary.

Properties Can Have Default Values

A default value is another useful feature of properties. You will notice that when you place a component on a form, many properties that are displayed in the Object Inspector already contain values. These are the default values as defined by the component writer. Assigning default values makes life easier for the component user. All properties should have a default value, if possible. This enables the user to change only specific properties and leave the rest alone. Certain types of properties (such as string properties) do not lend themselves to default values, but most do.


NOTE: String properties cannot have a default value.
Like the read and write methods, the default value is set when the property is declared. Let's go back to the component's FlashRate property. Declaring the FlashRate property with a default value would look like this:



property FlashRate : Integer
  read FFlashRate write SetFlashRate default 800;



Now, when the FlashingLabel component is displayed in the Object Inspector, the value of 800 (milliseconds in this case) will already be displayed for the FlashRate property.



NOTE: Setting a default value for the property displays only the default value in the Object Inspector. It does not set the value of the underlying data field for the property. You must still assign the default value to the data field in the component's constructor. For example, the constructor for the FlashingLabel component would look like this:
constructor TFlashingLabel.Create(AOwner : TComponent);
begin
  inherited;
  FFlashRate := 800;
  { Other things here. } 
end;



Be sure to set the appropriate values for all class data fields that correspond to properties with default values.




If you don't want to use a default value for a property, omit the default specifier in the property's declaration.


Properties Can Be Published, Public, or Private

Some properties are available at design time. These properties can be modified at design time through the Object Inspector and can also be modified or read at runtime. These properties are said to be published. Simply put, a published property is one that shows up in the Object Inspector at design time. Any properties located in the published section of the component's class declaration will be displayed in the Object Inspector at design time.

Other properties, called public properties, are runtime-only. These properties can't be accessed at design time (they don't show up in the Object Inspector). Properties of this type are declared in the public section of the component's class declaration.

Private properties are properties that are used internally by the component and are not available to the component users. Private properties are declared in the protected or private sections in the component's class declaration.

Writing Methods for Components

Writing methods for components is no different than writing methods for any Object Pascal class. Your component's methods can be private, protected, or public. It pays to keep in mind access levels as you write components.

Determining what methods to make public is easy. A public method is one that users of your component can call to cause the component to perform a specific action. The use of private versus protected methods is more difficult to decide. After programming for a while, you will be better able to recognize the situations when protected access should be used instead of private access.

Generally speaking, though, use private methods for tasks that are internal to the component and should not be accessible to derived classes. Use protected methods for tasks that are internal to the component, but are tasks that derived classes might want to alter to provide additional functionality to the component.


NOTE: Read and write methods for properties are usually made protected. Thus, you enable classes derived from your component to modify the behavior of the read and write methods by overriding the method.

As I said earlier, methods of components are just methods and, for the most part, can be treated as regular class member functions.


Adding Functionality to TFlashingLabel

A little later today I'll talk about events and how to write them, but for now you have enough information to write your first component. The FlashingLabel component has the following features:

First, I'll show you the complete FlashingLabel unit. After that, I'll go over what is happening with the code. Listing 20.2 shows the FlashingLabel unit.

Listing 20.2. FLASHINGLABEL.PAS.
unit FlashingLabel;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls;
type
  TFlashingLabel = class(TCustomLabel)
  private
    { Private declarations } 
    FFlashEnabled : Boolean;
    FFlashRate    : Integer;
    Timer         : TTimer;
  protected
    { Protected declarations } 
    { Protected write methods for the properties. } 
    procedure SetFlashEnabled(AFlashEnabled : Boolean);
    procedure SetFlashRate(AFlashRate : Integer);
    { OnTimer event handler. } 
    procedure OnTimer(Sender : TObject); virtual;
  public
    { Public declarations } 
    constructor Create(AOwner : TComponent); override;
  published
    { Published declarations } 
    { The component's properties. } 
    property FlashEnabled : Boolean
      read FFlashEnabled write SetFlashEnabled default True;
    property FlashRate : Integer
      read FFlashRate write SetFlashRate default 800;
    { All the properties of TCustomLabel redeclared. } 
    property Align;
    property Alignment;
    property AutoSize;
    property BiDiMode;
    property Caption;
    property Color;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property FocusControl;
    property Font;
    property ParentBiDiMode;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowAccelChar;
    property ShowHint;
    property Transparent;
    property Layout;
    property Visible;
    property WordWrap;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
  end;
procedure Register;
implementation
constructor TFlashingLabel.Create(AOwner : TComponent);
begin
  inherited;
  { Set the data fields to their default values. } 
  FFlashEnabled := True;
  FFlashRate    := 800;
  { Initialize the timer object. } 
  Timer := TTimer.Create(Self);
  { Set the timer interval using the flash rate. } 
  Timer.Interval := FFlashRate;
  { Assign our own OnTimer event handler to the
  { TTimer OnTimer event. } 
  Timer.OnTimer := OnTimer;
end;
procedure TFlashingLabel.SetFlashEnabled(AFlashEnabled : Boolean);
begin
  { Set FFlashEnabled data field. } 
  FFlashEnabled := AFlashEnabled;
  { Don't start the timer if the component is on a form
  { in design mode. Instead, just return. } 
  if csDesigning in ComponentState then
    Exit;
  { Start the timer. } 
  Timer.Enabled := FFlashEnabled;
  { If flashing was turned off, be sure that the label
  { is visible. } 
  if not FFlashEnabled then
    Visible := True;
end;
procedure TFlashingLabel.SetFlashRate(AFlashRate : Integer);
begin
  { Set the FFlashRate data field and the timer interval. } 
  FFlashRate := AFlashRate;
  Timer.Interval := AFlashRate;
end;
procedure TFlashingLabel.OnTimer(Sender : TObject);
begin
  { If the component is on a form in design mode,
  { stop the timer and return. } 
  if csDesigning in ComponentState then begin
    Timer.Enabled := False;
    Exit;
  end;
  { Toggle the Visible property each time the timer
  { event occurs. } 
  Visible := not Visible;
end;
procedure Register;
begin
  RegisterComponents(`Samples', [TFlashingLabel]);
end;
END.

The Class Declaration

First, let's take a look at the class declaration. Notice that the private section declares three class data fields. The first two, FFlashEnabled and FFlashRate, are the data fields associated with the FlashEnabled and FlashRate properties. The third declaration looks like this:

Timer : TTimer;

This declares a pointer to a TTimer object. The TTimer object is used to regulate the flash rate.


NOTE: I haven't discussed timers yet. A timer is set up to fire at a specified interval (in milliseconds). When a timer fires, a WM_TIMER message is sent to the window that owns the timer and, as a result, the OnTimer event is triggered. For example, if you set the timer interval to 1,000 milliseconds, your OnTimer event will be called nearly every second. I should point out that the WM_TIMER message is a low-priority message and can be preempted if the system is busy. For this reason, you can't use a regular timer for mission-critical operations. Still, for non-critical timings, the TTimer does a good job. (For more information on timers, refer to the Delphi help under TTimer or the WM_TIMER message.)

The protected section of the class declaration contains declarations for the write methods for the FlashRate and FlashEnabled properties. The protected section also declares the OnTimer function. This function is called each time a timer event occurs. It is protected and declared as virtual, so derived classes can override it to provide other special handling when a timer event occurs. For example, a derived class might want to change the color of the text each time the label flashes. Making this method virtual enables overriding of the function to add additional behavior.

The Published Section

Finally, notice the published section. This section contains declarations for the FlashEnabled and FlashRate properties. In the case of the FlashEnabled property, the read specifier uses direct access, the write specifier is set to the SetFlashEnabled method, and the default value is set to True. The FlashRate property uses a similar construct.


Notice that following the two property declarations, I have declared all the properties of TCustomLabel that I want republished. If you don't perform this step, the usual properties of a label component won't be available to the class at runtime nor later at design time (after you install the component to the Component palette).

The Implementation Section

Now turn your attention to the implementation section. First you see the TFlashingLabel Create constructor. Here you see that the default values for the class data fields representing the FlashEnabled and FlashRate properties are assigned. Remember that you declared default values for these properties, but that affects only the way the property is displayed in the Object Inspector. So you must assign the actual values in the constructor.

Notice these three lines (comments removed):

Timer := TTimer.Create(Self);
Timer.Interval := FFlashRate;
Timer.OnTimer := OnTimer;

The first line creates an instance of the TTimer object. The second line assigns the value of the FFlashRate data field to the Interval property of the Timer component, which sets the timer interval that will be used to flash the text on the label. Finally, the last line assigns the OnTimer function to the OnTimer event of the Timer object. This ensures that when a timer event occurs, the component will receive notification.

The SetFlashEnabled procedure is the write method for the FlashEnabled property. This procedure first assigns the FFlashEnabled data field to the value of AFlashEnabled. Next, the Enabled property of the Timer object is set according to the value of AFlashEnabled. By writing to the FlashEnabled property, the user of the component can turn the flashing on or off. This function also contains a code line that sets the Visible property to True when flashing is disabled. This eliminates the situation in which the flashing could be turned off in mid-flash and, as a result, keep the text hidden.

The SetFlashRate Procedure

Now turn your attention to the SetFlashRate procedure. This is the write method for the FlashRate property. The user can assign a value to the FlashRate property to control how fast the component flashes. (A rate of 1200 is slow, and a rate of 150 is very fast.) This function contains these lines:


FFlashRate := AFlashRate;
Timer.Interval := AFlashRate;

This procedure simply assigns the value of AFlashRate first to the FFlashRate data field and then to the Interval property of the Timer object. This ensures that if the user changes the flash rate at runtime, the flash rate will change accordingly.

The OnTimer method doesn't require much explanation. This method is called in response to an OnTimer event. All you do is toggle the component's visible state each time a timer event occurs. In other words, if the FlashRate were set to 1000, the timer events would occur nearly every second. When the first timer event occurs, the component is hidden. A second later, the next timer event occurs and the component is shown. This continues until the FlashEnabled property is set to False or until the application closes. Finally, at the end of the unit you see the component registration.

Enter the code from Listing 20.2 into the FlashingLabel.pas file that Delphi created for you earlier (when you used the New Component dialog box). Don't worry about typing the comment lines if you don't want to. A bit later I'll show you how to test the component to see whether it works.


TIP: Delphi's class completion makes declaring read and write methods for properties easy. Let's say, for example, that you typed this property declaration:
property FlashRate : Integer
  read FFlashRate write SetFlashRate;

The next step would probably be to declare and define the SetFlashRate method. Guess what? Delphi will do it for you! Just press Ctrl+Shift+C and Delphi will create any read or write methods you haven't yet defined, and it will even add a declaration for the data field (FFlashRate in this example).


TIP: If you have Delphi Professional or Client/Server, you can copy the property redeclarations directly from the VCL source. Just open \Delphi 4\Source\Vcl\ StdCtrls.pas, copy the properties list from the TLabel class declaration, and paste it into your class declaration.

The ComponentState Property

I'm getting a little ahead of myself here, but I want to point out some code that I didn't discuss in the analysis of Listing 20.2. There is one important line in the SetFlashEnabled function that requires explanation. Here's that line, and the one that follows:

if csDesigning in ComponentState then
  Exit;

This code checks whether the component is being used on a form at design time. If the form is being used at design time, you need to prevent the timer from starting. When a component is placed on a form in the Form Designer, it does not necessarily have full functionality--the Form Designer can't fully approximate a running program. In this case, you don't want the timer to run while the component is being used in design mode.


NOTE: You could write the TFlashingLabel component so that the label flashes at design time as well as runtime. It is easier not to deal with that now, plus it gives me a chance to talk about the ComponentState property.

All components have a property called ComponentState. The ComponentState property is a set that indicates, among other things, whether the component is being used at design time. If the set includes csDesigning, you know that the component is being used on a form in the Form Designer. In this case, when you determine that the component is being used at design time, you can return from the OnTimer function without starting the timer.

This code, then, checks whether the component is being used on a form in the Form Designer. If the component is being used in the Form Designer, the timer is turned off, and the method exits without performing the remaining code in the method. The timer, by default, will be started in the TFlashingLabel constructor, so it needs to be immediately turned off if the component is being used at design time.

Testing the Component

Ultimately, you will add your newly created component to the Component palette. First, though, you must test your component to be sure that it compiles and that it functions as you intended. This is a vital step in component development, and one that many component writers overlook. There's no point in being in a hurry to add a new component to the Component palette. First be sure that the component works as expected and then worry about adding the component to the Component palette.

To test your component, write an application that will serve as a testing ground. Because you can't drop the component on a form from the Component palette, you have to create the component manually. In this case, because your FlashingLabel component has two properties, you want to make sure that each property works.

For that reason, your test program will need to turn the flashing mode on and off. In addition, the test program should enable you to set several flashing rates to see whether the FlashRate property performs as designed. Figure 20.2 shows the test program running. This will give you an idea of what you'll be trying to accomplish.

FIGURE 20.2. The test program running.


Now that you've had a peek at the test program, let's create it. As always, start with a blank form. First, add the check box and radio group components as you see them in Figure 20.2:



1. Change the form's Name property to MainForm and the Caption property to FlashingLabel Test Program.

2. Using Figure 20.2 as a pattern, add a CheckBox component to the form. Change its Name property to FlashBox, its Caption property to Flash, and its Checked property to True.

3. Double-click the check box to create an event handler for the OnClick event. When the Code Editor displays the OnClick handler, type this code at the cursor (the name of the FlashingLabel component will be Flasher):

Flasher.FlashEnabled := FlashBox.Checked;


This will enable or disable flashing depending on the state of the check box.

4. Place a RadioGroup component on the form. Change its Name property to Group and its Caption property to Flash Speed.

5. Double-click on the Value column next to the Items property. When the String Editor is displayed, type these lines:

Slow
Medium
Fast
Light Speed



Click OK to close the String Editor. The strings you typed will be displayed as radio buttons in the group box.

6. Set the ItemIndex property to 1. The Medium radio button will be selected.

7. Double-click the radio group component. The Code Editor will display the OnClick event handler for the group box. Type the following code at the cursor:

case Group.ItemIndex of
  0 :  Flasher.FlashRate := 1200;
  1 :  Flasher.FlashRate := 800;
  2 :  Flasher.FlashRate := 400;
  3 :  Flasher.FlashRate := 150;
end;


This will set the FlashRate value of the FlashingLabel component based on the radio button selected.

8. Save the project in the same directory where your TFlashingLabel component resides. Save the main form's unit as FlshTstU and the project as FlashTst (remember, I use short filenames for the book's code, but you can use long filenames if you want).

Okay, now you add the component itself. Because this component isn't yet visual (you can't add it from the Component palette), you will have to add it manually.

1. Click the Add to Project button (from the toolbar, from the main menu, or from the Project Manager context menu). When the Add to Project dialog box is displayed, choose the FlashingLabel.pas file and click OK.

2. Navigate to the top of the unit and add FlashingLabel to the unit's uses list.

3. Add this declaration in the private section of the TMainForm class:

Flasher : TFlashingLabel;


4. Double-click the form's background to create an OnCreate event handler for the form. Type the following code in the event handler:

Flasher := TFlashingLabel.Create(Self);
Flasher.Parent := Self;


Flasher.SetBounds(20, 20, 200, 20);

Flasher.Font.Size := 16;
Flasher.Caption := `This is a test';
Flasher.FlashRate := 800;

Now you are ready to test the component. Click the Run button to compile and run the test program. If you encounter any compiler errors, carefully check your code and fix any errors the compiler points out.

When the program runs, click the Flash check box to turn the flashing on or off. Change the flash rate by choosing one of the radio buttons in the group box. Hey, it works! Congratulations, you've written your first component.

Adding the Component to the Component Palette

After the component is working properly and you are satisfied with it, you can add it to the Component palette. To add the FlashingLabel component to the Component palette, choose Component|Install Component. The Install Component dialog box will appear. This dialog box enables you to add a component to a package. Figure 20.3 shows the Install Component dialog box.

FIGURE 20.3. The Install Component dialog box.

Okay, you're ready to add the FlashingLabel component to the Component palette. Perform the following steps:

1. Choose Component|Install Component from the main menu. The Install Component dialog box is displayed.

2. Click the Browse button to the right of the Unit file name field. When the Unit file name dialog box comes up, locate the FlashingLabel.pas file and click Open.

3. Now look at the Package file name field. It should contain the file DCLUSR40.DPK. If not, click the drop-down button and select DCLUSR40.DPK from the list. If you don't see this package listed, click the Browse button and find the file (it is in the \Delphi 4\Lib directory).

4. Click OK to close the Install Component dialog box. Delphi displays a message telling you that it is about to build and install the package. Click Yes to continue.

5. Delphi builds and installs the package. When the process is complete, Delphi displays a message box telling you that the TFlashingLabel component has been registered.

Your component will now appear on the Samples page of the Component palette. Check the Samples page on the Component palette and you will see a button that has the Delphi default component icon. If you pause over the button, the tooltip says FlashingLabel.

Start a new project and test the FlashingLabel component by dropping it on the form. Note that all the usual properties of a Label component are present in the Object Inspector, as well as the FlashRate and FlashEnabled properties. Note also that the default values you specified for these properties are displayed in the Object Inspector.

I want to explain what you did in step 3. Delphi has a default package called DCLUSR40 that can be used to install individual components. I had you install the FlashingLabel component in this package primarily because TFlashingLabel is a single component (not part of an overall component library), and that's what this package is for. You could have created a new package rather than use the DCLUSR40 package, but it is easier to use the package provided.

Adding a Custom Bitmap to the Component's Button

You might have noticed a problem with your newly installed component: The button for the FlashingLabel component on the Component palette has the default Delphi bitmap. You can't have that! Fortunately, you can specify a bitmap for your new component. You have to create a bitmap and place it in a compiled resource file (a .dcr file).


TIP: Sometimes you might want to take the button for the base class and modify it slightly to represent your new component. In that case, start the Image Editor and open one of the .dcr files in the \Delphi 4\Lib\Obj directory. You'll have to hunt to find the exact bitmap. For example, the bitmap for the Label component is in the file called Stdreg.dcr. Open that file, copy the TLABEL bitmap to the Clipboard, begin a new resource file, and paste the bitmap from the Clipboard into a new bitmap resource called TFLASHINGLABEL. Modify the bitmap as desired and then save the resource project.

The bitmap for the component's button must be 24¥24 pixels. Most of the time a 16-color bitmap is sufficient. Delphi uses the lower-left pixel in the bitmap for the transparent color, so keep that in mind when designing your component's bitmap. (Delphi bitmaps use the dark yellow color as the transparent color, so you can follow that convention if you want.)


NOTE: Be sure you create a bitmap resource and not an icon resource. The Component palette buttons are often called icons, but their images are bitmaps and not icons.

After you create the resource file, Delphi automatically adds the component's bitmap to the Component palette when you install the component's package. For this to happen, you must follow a specific naming convention for the bitmap.

Specifically, the resource file must have a bitmap resource that exactly matches the classname of the component. For example, to create a bitmap for the FlashingLabel component's button, you use Image Editor to create a resource file that contains a bitmap resource called TFLASHINGLABEL. You can name the resource file anything you want.

Now that you have created a resource file, you must tell Delphi to link the resource file with the component's code. To do that, add a line like this to your component's source code:

{$R Flashing.res} 

The $R compiler directive tells the compiler to include the contents of a resource file with the unit's compiled code. Now rebuild the package. If you've done everything correctly, the bitmap you created for the button will show up in the Component palette.


NOTE: Notice that the filename extension of the resource file in the preceding code snippet is .res. The .res extension is used interchangeably with the .dcr extension. Some component vendors use a unique naming convention for their compiled resources and don't use either .res or .dcr. The filename extension is not important when using the $R compiler directive. What is important, however, is that the file contains valid resources.



As an alternative, you can also add the $R directive directly to the package source. Most of the time this is not necessary, though.




CAUTION: You can add compiled resources only in one place. Place the $R compiler directive in either the package source or in the component's unit, but not both. If you add the same resources more than once, you will get a compiler error and the package will not install.


TIP: If you have a library of several components, you can use one resource file for all of the component's bitmaps. You don't need to have a separate resource file for each component in your library.

Writing Events for Components

Writing events requires some planning. When I speak of events, I am talking about two possibilities. Sometimes an event occurs as the result of a Windows message, and sometimes an event occurs as a result of a change in the component. An event triggered by a Windows message is more or less out of your control. You can respond to this type of event, but you generally don't initiate the event. The second type of event is triggered by the component itself. In other words, as the component writer, you are in control of when this type of event occurs.

Working with events at this level can be confusing. I'll try to get past that confusion and show you how events can be used on a practical level. To do this, let's add an event to the TFlashingLabel class. First, however, let's cover a few of the event basics.

Events Overview

To begin with, you should understand that events are properties, and, as such, they have all the features that a regular property has. Events use a private data field to store their values, as do other properties. In the case of an event, the underlying data field contains the address of a function that will be called when the specified event occurs. Like properties, events can be published or non-published. Published events show up in the Object Inspector just as published properties do.


Second, events are method pointers. Method pointers are like function super-pointers: They can point not only to a function in a class instance, but they can also point to a function in an instance of an unrelated class. As long as the function declarations match (the same return type and the same function parameters), the method pointer happily calls the function regardless of where it's located. For example, the OnClick event of a TLabel object can point to an event-handling function in a TEdit object, a TForm object, a TListBox object, and so on. Method pointers are more complicated than that, but I won't go into the gory details.


NOTE: Event handlers must always be procedures. An event might pass one or more parameters depending on the type of the event, but it can't return a value. You can get information back from the event handler, though, by using one or more var parameters and enabling the user to change those parameters to achieve a particular behavior.

You might deal with events on any one of several levels. For example, you might want to override the base class event handler for a particular event to add some functionality. Let's say you want something special to happen when the user clicks on your component with the mouse. There's no use going to all the trouble of creating a new event for a mouse click because the event already exists in the form of the base class's OnClick event. You just tag onto that event rather than create a new event to catch the mouse click.

Another way you might deal with events is by creating an event that you trigger from within the component. I will describe this type of event first. As I said, you're going to add an event to the TFlashingLabel component that you created earlier. Adding this event requires that you also add a new property. The event will be called OnLimitReached, and the new property will be called FlashLimit. This event will be fired after the component flashes the number of times specified by FlashLimit. If FlashLimit is 0 (the default), the OnLimitReached event will never be fired.

Writing a user-defined event for a component can be divided into five basic tasks:

1. Determine the event type.

2. Declare the underlying data field.

3. Declare the event.

4. Create a virtual method that is called when the event is to be triggered.

5. Write code to trigger the event.

Let's walk through these steps so that you better understand what is involved.


Determining the Event Type

Earlier, when I discussed properties, I said that a property is of a specific type. The same is true of events. In the case of events, though, the type is a method pointer that includes a description of the event handler's parameters. Yesterday, Day 19, "Creating and Using DLLs," I talked a little about function pointers when I discussed DLLs.

As I have said several times, there are two basic types of events. One is the notification event. This event tells you that something happened, but it doesn't give you any other details. The function declaration of the event handler for a notification event looks like this:

procedure Clicked(Sender : TObject);

The only information you get in a notification event is the sender of the event. Delphi provides the TNotifyEvent type for notification events. Any events you create that are notification events should be of the TNotifyEvent type.

The other type of event is an event that has more than one parameter and actually passes information to the event handler. If you want to, you can create this type of event for your components. Let's say you want to use an event handler that is prototyped like this:

procedure LimitReached(Sender : TObject; var Stop : Boolean);

Using this type of event handler enables the user to modify the Stop parameter, thereby sending information back to the component. If you are going to create events that have parameters, you need to declare your own method type.

Let's say you want to write an event type for the preceding method declaration and that the event type will be named TLimitReachedEvent. It would look like this:

TLimitReachedEvent =
  procedure(Sender : TObject; var Stop : Boolean) of object;

Although that's kind of confusing, all you have to do is copy this pattern and then add or remove parameters as needed. After you have the event type defined, you can declare an event to be of the type TLimitReachedEvent. (This won't make a lot of sense until you work through the whole process, so bear with me.)


NOTE: Place the declaration for a new event type in the type section of the unit just above the class declaration. For example:
unit FlashingLabel;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls;
type
  TLimitReachedEvent =
    procedure(Sender : TObject; var Stop : Boolean) of object;
  TFlashingLabel = class(TCustomLabel)
  private
{ Rest of unit follows. } 



Try to determine to the best of your ability what type of events (if any) you need for your component. If you need only notification events, you will create your events to be of the TNotifyEvent type. If your events will pass parameters to the event handler, you need to define your own event type.

Declaring the Underlying Data Field

Declaring the underlying data field is the simple part. All you have to do is declare a private data field with the same type as your event. It looks like this:

FOnLimitReached : TLimitReachedEvent;

As with properties, the data field's name is the same as the event name with the F on the front.

Declaring the Event

Now that you have determined the event type, you can declare the event for the component. The declaration for an event looks almost identical to the declaration for any other property. Here's a typical declaration for an event:

property OnSomeEvent : TNotifyEvent
  read FOnSomeEvent write FOnSomeEvent;

For the most part, this looks like the property declarations that you dealt with earlier. Notice that there is no default specifier. Notice also that the read and write specifiers both point to the FOnSomeEvent data field. This illustrates that events use direct access and do not use read and write methods. Finally, notice that in this example the event's type is TNotifyEvent.

The OnLimitReached event will pass parameters, so you must define a special type and apply it to the event. Given that, your declaration for the OnLimitReached event looks like this:

property OnLimitReached : TLimitReachedEvent

  read FOnLimitReached write FOnLimitReached;

Soon I'll show you the entire unit so that you can see it in perspective. (If you want to look ahead, check out Listing 20.3.)

Creating a Virtual Method to Trigger the Event

Creating a virtual method to trigger the event requires explanation. You will trigger the event as a result of some change within the component. In this case, you are going to trigger the event when the number of times the component has flashed reaches the value of the FlashLimit property. You trigger an event by calling the event:

var
  Stop : Boolean;
begin
  Stop := False;
  FOnLimitReached(Self, Stop);

You trigger the event from one of several places in the component based on different factors. To centralize this trigger, you create a virtual method that triggers the event. The virtual method will have the same name as the event, minus the On part and prepended with Do.


NOTE: There are two popular naming conventions for naming methods that generate events. The first naming convention removes the On prefix and replaces it with Do. In the case of the OnLimitReached event, the virtual method that generates the event would be called DoLimitReached. The second naming convention drops the On prefix and leaves it at that. I've used both naming conventions, but I have a slight preference for the former.

First, declare the method in the class declaration:

procedure DoLimitReached; virtual;

Then, write the method that actually triggers the event. That method will look like this:

procedure TFlashingLabel.DoLimitReached;
var
  Stop : Boolean;
begin
  Stop := False;
  if Assigned(FOnLimitReached) then
    FOnLimitReached(Self, Stop);
  FlashEnabled := not Stop;
end;

There are several issues to note here. First, notice that you set up the default value of the Stop parameter. If the user doesn't modify the parameter, the value of Stop will be False. The Stop parameter is used to determine whether the flashing should stop when the FlashLimit value is reached. In the event handler for the event (in the application that uses the component), the user can set Stop to True to cause the flashing to stop or can leave the Stop parameter as is to enable the flashing to continue. Notice that Self is passed in the first parameter, setting the Sender parameter to the component's pointer.

Now, notice the statement that triggers the event:

if Assigned(FOnLimitReached) then

    FOnLimitReached(Self, Stop);

If the component user has attached an event handler to the event, you call the event handler. If no event handler has been attached, you do the default handling for the event. (In this case, there is no default handling.) It is important to enable the user to ignore the event if he or she so chooses. This code enables that choice.


NOTE: One concept that is difficult to grasp when writing events is that the component itself does not provide the event handler. The application using the component provides the event handler; you merely provide the mechanism by which the event handler can be called when the event occurs.


CAUTION: If the user hasn't defined an event handler for your event, the event will be nil (no value assigned). Never call an event handler without first ensuring that the event has been assigned. Attempting to call an event that has not been assigned will result in an Access Violation in your component.

As I mentioned earlier, DoLimitReached is a virtual method. It is a virtual method because derived classes might want to redefine the event's default behavior. Because you were kind enough to make the method virtual, any components derived from your component only have to override the DoLimitReached function to change the default event-handling behavior. This makes it easy to change the behavior of the event without having to hunt through the code and locate every place the event is triggered. The event is triggered only here in the DoLimitReached method.

Writing Code That Triggers the Event

Somewhere in the component there must be code to call the DoLimitReached method (which, in turn, triggers the event). In the case of the TFlashingLabel component, you call DoLimitReached from the OnTimer procedure. Here's how the function looks after being modified to trigger the event:

procedure TFlashingLabel.OnTimer(Sender : TObject);

begin
  { If the component is on a form in design mode,
  { stop the timer and return. } 
  if csDesigning in ComponentState then begin
    Timer.Enabled := False;
    Exit;
  end;
  { Toggle the Visible property each time } 
  { the timer event occurs. } 
  Visible := not Visible;
  { Trigger the event if needed. Only increment } 
  { the counter when the label is visible. } 
  if (FFlashLimit <> 0) and Visible then begin
    { Increment the FlashCount data field. } 
    Inc(FlashCount);
    { If the FlashCount is greater than or equal to   } 
    { the value of the FlashLimit property, reset the } 
    { FlashCount value to 0 and trigger the event.    } 
    if FlashCount >= FFlashLimit then begin
      FlashCount := 0;
      DoLimitReached;
    end;
  end;
end;

As you can see, when the FlashLimit is reached, the DoLimitReached method is called and the event is triggered. By the way, you count only every other OnTimer event. If you incremented the counter every time the OnTimer function was called, you would get an inaccurate count because the OnTimer event is fired twice for every flash (on and off). The Count variable is a class data field and FlashLimit is a property.

Overriding Base Class Events

The preceding discussion ties in with something else I want to mention briefly. If you want to override the default behavior of one of the events defined in the base class, all you have to do is override its event-triggering function as I've described. Let's say you want to override the default OnClick event to make the speaker beep when the component is clicked. All you do is override the Click function of the base class like this:

procedure TFlashingLabel.Click;

begin
  { Beep the speaker and then call the base class } 
  { Click method for the default handling. } 
  MessageBeep(-1);
  inherited;
end;

Because this function is declared as dynamic in the base class, it will be called automatically any time the component is clicked with the mouse. It will work only when the component is visible, so keep that in mind if you try to click the component while it is flashing. The call to MessageBeep is there just to prove that it works.

Putting It All Together

You took a peek at most of the new and improved TFlashingLabel component in the preceding pages, but let's look at the entire component to put it all in perspective. Listing 20.3 shows the source file for the finished TFlashingLabel component. Study the implementation of the OnLimitReached event to gain an understanding of how events should be implemented in your components. Listing 20.4 contains the main unit of the modified test program. The MainFormLimitReached method shows use of the OnLimitReached event.

LISTING 20.3. FlashingLabel.pas (NEW AND IMPROVED).

unit FlashingLabel;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, ExtCtrls;
type

  TLimitReachedEvent =
    procedure(Sender : TObject; var Stop : Boolean) of object;
  TFlashingLabel = class(TCustomLabel)
  private
    { Private declarations } 
    FFlashEnabled   : Boolean;
    FFlashLimit     : Integer;
    FFlashRate      : Integer;
    FOnLimitReached : TLimitReachedEvent;
    FlashCount      : Integer;
    Timer           : TTimer;
  protected
    { Protected declarations } 
    { Protected write methods for the properties. } 
    procedure SetFlashEnabled(AFlashEnabled : Boolean);
    procedure SetFlashRate(AFlashRate : Integer);
    procedure DoLimitReached; virtual;
    procedure Click; override;
    { OnTimer event handler. } 
    procedure OnTimer(Sender : TObject); virtual;
  public
    { Public declarations } 
    constructor Create(AOwner : TComponent); override;
  published
    { Published declarations } 
    { The component's properties. } 
    property FlashEnabled : Boolean
      read FFlashEnabled write SetFlashEnabled default True;
    property FlashRate : Integer
      read FFlashRate write SetFlashRate default 800;
    property FlashLimit : Integer
      read FFlashLimit write FFlashLimit default 0;
    property OnLimitReached : TLimitReachedEvent
      read FOnLimitReached write FOnLimitReached;
    { All the properties of TCustomLabel redeclared. } 
    property Align;
    property Alignment;
    property AutoSize;
    property BiDiMode;

property Caption;

    property Color;
    property Constraints;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property FocusControl;
    property Font;
    property ParentBiDiMode;
    property ParentColor;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowAccelChar;
    property ShowHint;
    property Transparent;
    property Layout;
    property Visible;
    property WordWrap;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnStartDock;
    property OnStartDrag;
  end;
procedure Register;
implementation
constructor TFlashingLabel.Create(AOwner : TComponent);
begin
  inherited;
  { Set the data fields to their default values. } 
  FFlashEnabled := True;
  FFlashRate    := 800;
  FFlashLimit   := 0;
  FlashCount    := 0;
  { Initialize the timer object. } 
  Timer := TTimer.Create(Self);
  { Set the timer interval using the flash rate. } 
  Timer.Interval := FFlashRate;

{ Assign our own OnTimer event handler to the

  { TTimer OnTimer event. } 
  Timer.OnTimer := OnTimer;
end;
procedure TFlashingLabel.SetFlashEnabled(AFlashEnabled : Boolean);
begin
  { Set FFlashEnabled data field. } 
  FFlashEnabled := AFlashEnabled;
  { Don't start the timer if the component is on a form
  { in design mode. Instead, just return. } 
  if csDesigning in ComponentState then
    Exit;
  { Start the timer. } 
  Timer.Enabled := FFlashEnabled;
  { If flashing was turned off, be sure that the label
  { is visible. } 
  if not FFlashEnabled then
    Visible := True;
end;
procedure TFlashingLabel.SetFlashRate(AFlashRate : Integer);
begin
  { Set the FFlashRate data field and the timer interval. } 
  FFlashRate := AFlashRate;
  Timer.Interval := AFlashRate;
end;
procedure TFlashingLabel.OnTimer(Sender : TObject);
begin
  { If the component is on a form in design mode,
  { stop the timer and return. } 
  if csDesigning in ComponentState then begin
    Timer.Enabled := False;
    Exit;
  end;
  { Toggle the Visible property each time } 
  { the timer event occurs. } 
  Visible := not Visible;
  { Trigger the event if needed. Only increment } 
  { the counter when the label is visible. } 
  if (FFlashLimit <> 0) and Visible then begin
    { Increment the FlashCount data field. } 
    Inc(FlashCount);
    { If the FlashCount is greater than or equal to   } 
    { the value of the FlashLimit property, reset the } 
    { FlashCount value to 0 and trigger the event.    } 
    if FlashCount >= FFlashLimit then begin
      FlashCount := 0;
      DoLimitReached;
    end;
  end;
end;
procedure TFlashingLabel.DoLimitReached;
var
  Stop : Boolean;
begin
  Stop := False;
  if Assigned(FOnLimitReached) then
    FOnLimitReached(Self, Stop);
  FlashEnabled := not Stop;
end;
procedure TFlashingLabel.Click;
begin
  { Beep the speaker and then call the base class } 
  { Click method for the default handling. } 
  MessageBeep(-1);
  inherited;
end;
procedure Register;

begin

  RegisterComponents(`Samples', [TFlashingLabel]);
end;
end.

LISTING 20.4. FlshTstU.pas.

unit FlshTstU;
interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, Flashing;
type
  TMainForm = class(TForm)
    FlashBox: TCheckBox;

Group: TRadioGroup;

    procedure FormCreate(Sender: TObject);
    procedure GroupClick(Sender: TObject);
    procedure FlashBoxClick(Sender: TObject);
  private
    { Private declarations } 
    Flasher : TFlashingLabel;
    procedure OnLimitReached(Sender : TObject; var Stop : Boolean);  
public
    { Public declarations } 
  end;
var
  MainForm: TMainForm;
implementation
{$R *.DFM} 
procedure TMainForm.FormCreate(Sender: TObject);
begin
  Flasher := TFlashingLabel.Create(Self);
  Flasher.Parent := Self;
  Flasher.SetBounds(20, 20, 200, 20);
  Flasher.Font.Size := 16;
  Flasher.Caption := `This is a test';
  Flasher.FlashRate := 800;
  Flasher.FlashLimit := 5;
  Flasher.OnLimitReached := OnLimitReached;
end;
procedure TMainForm.OnLimitReached(Sender : TObject; var Stop : Boolean);
begin
  { The OnLimitReached event handler. Set Stop to } 
  { true to stop flashing or leave as-is to } 
  { continue flashing.} 
  Stop := True;
end;
procedure TMainForm.GroupClick(Sender: TObject);
begin
  case Group.ItemIndex of
    0 :  Flasher.FlashRate := 1200;
    1 :  Flasher.FlashRate := 800;
    2 :  Flasher.FlashRate := 400;
    3 :  Flasher.FlashRate := 150;
  end;
end;
procedure TMainForm.FlashBoxClick(Sender: TObject);
begin
  Flasher.FlashEnabled := FlashBox.Checked;
end;
end.

This component and the FlashTst test program are included with the book's code. However, the component's source unit is called Flashing.pas.

Run the test program to see whether the event works as advertised. Experiment with the test program to get a better feeling for how the event and the event handler work. Notice that the speaker beeps when you click on the label (as long is it is visible). This is because of the overridden dynamic method called Click. I added this function to illustrate how to override a base class event.


NOTE: If you want to install the new and improved FlashingLabel component on the Component palette, open the DCLUSR40 package and click on the Compile Package button. The old version of FlashingLabel will be updated to the new version.

Writing events takes time to master. You have to kiss a lot of frogs before being rewarded with the prince (or princess). In other words, there's no substitute for experience when it comes to writing events. You just have to get in there and do it. You'll probably hit a few bumps along the way, but it will be good experience, and you'll find yourself a better programmer for it.

Summary

Well, you're in the big league now. Writing components isn't necessarily easy, but when you have a basic understanding of writing components, the world is at your fingertips. If you are anything like me, you will learn to enjoy your nonvisual programming time as much as you enjoy the visual programming you do. If this chapter seems overwhelming, don't feel bad. You might have to let it soak in for a few days and then come back to it. You might have to read several different documents on writing components before it all comes together. Keep at it and it will eventually make sense.

Workshop

The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."

Q&A

Q Is there any reason I must know how to write components?
A No. You can use the basic components that Delphi gives you in your applications. You might never need to write components.

Q Can I buy components?

A Yes. A wide variety of sources exist for third-party component libraries. These libraries are sold by companies that specialize in VCL components. In addition, many freeware or shareware components are available from various online sources. Be sure to search the Internet for sources of VCL components.

Q Can I use my Delphi components with C++Builder?

A Yes. C++Builder has the capability to compile and install Delphi components.

Q Do I have to use read and write methods to store my property's value?

A No. You can use direct access and store the property's value directly in the underlying data field associated with the property.

Q What's the advantage of using a write method for my properties?

A Using a write method enables you to perform other operations when a property is written to. Writing to a property often causes the component to perform specific tasks. Using a write method enables you to carry out those tasks.

Q Why would I want a property to be public but not published?

A A published property is displayed in the Object Inspector at design time. Some properties don't have a design-time interface. Those properties should be made public so that the user can modify or read them at runtime but not at design time.

Q How do I test my component to be sure it works correctly before I install it?

A Write a test program and add the component's source file to your project. In the constructor of the main form, create an instance of your component. Set any properties that you need to set prior to the component being made visible. In the test program, manipulate your component's public properties to see whether everything works properly.

Q Do I have to write events for my components?

A Not necessarily. Some components make use of events; others do not. Don't go out of your way to write events for your components, but by the same token don't shy away from writing events when you need them.

Quiz

1. Must a property use a write method? Why or why not?
2. Must a property have an underlying data field? Why or why not?

3. Can you create a component by extending an existing component?

4. What happens if you don't specify a write specifier (either a write method or direct access) in a property's declaration?

5. What does direct access mean?

6. Must your properties have a default value? Why or why not?

7. Does the default value of the property set the underlying data field's value automatically?

8. How do you install a component on the Component palette?

9. How do you specify the button bitmap that your component will use on the Component palette?

10. How do you trigger a user-defined event?

Exercises

1. Review the FlashingLabel component source in Listing 20.3. Study it to learn what is happening in the code.

2. Remove the FlashingLabel component from the component library. Reinstall the FlashingLabel component.
3. Write a test program that uses three FlashingLabel components, all with different flash rates.

4. Change the bitmap for the FlashingLabel button on the Component palette to one of your own design.

5. Write a write method for the FlashLimit property of the FlashingLabel component so that the user can't enter a number greater than 100.

6. Change the OnLimitReached event for the FlashingLabel component to a regular notification event (Hint: Use TNotifyEvent).

7. Extra Credit: Write a component of your own design.

8. Extra Credit: Test your new component and install it on the Component palette.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.