Chapter 32
Object-oriented programming for component writers

If you have written applications with Delphi, you know that a class contains both data and code, and that you can manipulate classes at design time and at runtime. In that sense, you've become a component user.

When you create new components, you deal with classes in ways that application developers never need to. You also try to hide the inner workings of the component from the developers who will use it. By choosing appropriate ancestors for your components, designing interfaces that expose only the properties and methods that developers need, and following the other guidelines in this chapter, you can create versatile, reusable components.

Before you start creating components, you should be familiar with these topics, which are related to object-oriented programming (OOP):

Defining new classes

The difference between component writers and application developers is that component writers create new classes while application developers manipulate instances of classes.

A class is essentially a type. As a programmer, you are always working with types and instances, even if you do not use that terminology. For example, you create variables of a type, such as Integer. Classes are usually more complex than simple data types, but they work the same way: By assigning different values to instances of the same type, you can perform different tasks.

For example, it is quite common to create a form containing two buttons, one labeled OK and one labeled Cancel. Each is an instance of the class TButton, but by assigning different values to their Caption properties and different handlers to their OnClick events, you make the two instances behave differently.

Deriving new classes

There are two reasons to derive a new class:

In either case, the goal is to create reusable objects. If you design components with reuse in mind, you can save work later on. Give your classes usable default values, but allow them to be customized.

To change class defaults to avoid repetition

Most programmers try to avoid repetition. Thus, if you find yourself rewriting the same lines of code over and over, you place the code in a subroutine or function, or build a library of routines that you can use in many programs. The same reasoning holds for components. If you find yourself changing the same properties or making the same method calls, you can create a new component that does these things by default.

For example, suppose that each time you create an application, you add a dialog box to perform a particular operation. Although it is not difficult to recreate the dialog each time, it is also not necessary. You can design the dialog once, set its properties, and install a wrapper component associated with it onto the Component palette. By making the dialog into a reusable component, you not only eliminate a repetitive task, but you encourage standardization and reduce the likelihood of errors each time the dialog is recreated.

"Modifying an existing component," shows an example of changing a component's default properties.

Note: If you want to modify only the published properties of an existing component, or to save specific event handlers for a component or group of components, you may be able to accomplish this more easily by creating a component template.

To add new capabilities to a class

A common reason for creating new components is to add capabilities not found in existing components. When you do this, you derive the new component from either an existing component or an abstract base class, such as TComponent or TControl.

Derive your new component from the class that contains the closest subset of the features you want. You can add capabilities to a class, but you cannot take them away; so if an existing component class contains properties that you do not want to include in yours, you should derive from that component's ancestor.

For example, if you want to add features to a list box, you could derive your component from TListBox. However, if you want to add new features but exclude some capabilities of the standard list box, you need to derive your component from TCustomListBox, the ancestor of TListBox. Then you can recreate (or make visible) only the list-box capabilities you want, and add your new features.

"Customizing a grid," shows an example of customizing an abstract component class.

Declaring a new component class

In addition to standard components, Delphi provides many abstract classes designed as bases for deriving new components. Component creation starting points shows the classes you can start from when you create your own components.

To declare a new component class, add a class declaration to the component's unit file.

Here is the declaration of a simple graphical component:

type
  TSampleShape = class(TGraphicControl)
  end;

A finished component declaration usually includes property, event, and method declarations before the end. But a declaration like the one above is also valid, and provides a starting point for the addition of component features.

Ancestors, descendants, and class hierarchies

Application developers take for granted that every control has properties named Top and Left that determine its position on the form. To them, it may not matter that all controls inherit these properties from a common ancestor, TControl. When you create a component, however, you must know which class to derive it from so that it inherits the appropriate features. And you must know everything that your control inherits, so you can take advantage of inherited features without recreating them.

The class from which you derive a component is called its immediate ancestor. Each component inherits from its immediate ancestor, and from the immediate ancestor of its immediate ancestor, and so forth. All of the classes from which a component inherits are called its ancestors; the component is a descendant of its ancestors.

Together, all the ancestor-descendant relationships in an application constitute a hierarchy of classes. Each generation in the hierarchy contains more than its ancestors, since a class inherits everything from its ancestors, then adds new properties and methods or redefines existing ones.

If you do not specify an immediate ancestor, Delphi derives your component from the default ancestor, TObject. TObject is the ultimate ancestor of all classes in the object hierarchy.

The general rule for choosing which object to derive from is simple: Pick the object that contains as much as possible of what you want to include in your new object, but which does not include anything you do not want in the new object. You can always add things to your objects, but you cannot take things out.

Controlling access

There are five levels of access control--also called visibility--on properties, methods, and fields. Visibility determines which code can access which parts of the class. By specifying visibility, you define the interface to your components.

The Table 32.1, "Levels of visibility within an object," shows the levels of visibility, from most restrictive to most accessible:

Table 32.1   Levels of visibility within an object

Visibility

Meaning

Used for

private

Accessible only to code in the unit where the class is defined.

Hiding implementation details.

protected

Accessible to code in the unit(s) where the class and its descendants are defined.

Defining the component writer's interface.

public

Accessible to all code.

Defining the runtime interface.

automated

Accessible to all code. Automation type information is generated.

OLE automation only.

published

Accessible to all code and from the Object Inspector.

Defining the design-time interface.

Declare members as private if you want them to be available only within the class where they are defined; declare them as protected if you want them to be available only within that class and its descendants. Remember, though, that if a member is available anywhere within a unit file, it is available everywhere in that file. Thus, if you define two classes in the same unit, the classes will be able to access each other's private methods. And if you derive a class in a different unit from its ancestor, all the classes in the new unit will be able to access the ancestor's protected methods.

Hiding implementation details

Declaring part of a class as private makes that part invisible to code outside the class's unit file. Within the unit that contains the declaration, code can access the part as if it were public.

Here is an example that shows how declaring a field as private hides it from application developers. The listing shows two form units. Each form has a handler for its OnCreate event which assigns a value to a private field. The compiler allows assignment to the field only in the form where it is declared.

unit HideInfo;
interface

uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs;

type
  TSecretForm = class(TForm)                                           { declare new form }
    procedure FormCreate(Sender: TObject);
  private                                                          { declare private part }
    FSecretCode: Integer;                                       { declare a private field }
  end;

var
  SecretForm: TSecretForm;

implementation
procedure TSecretForm.FormCreate(Sender: TObject);
begin
  FSecretCode := 42;                                            { this compiles correctly }
end;
end.                                                                        { end of unit }

unit TestHide;                                               { this is the main form file }

interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs,
  HideInfo;                                               { use the unit with TSecretForm }

type
  TTestForm = class(TForm)
    procedure FormCreate(Sender: TObject);
  end;
var
  TestForm: TTestForm;

implementation
procedure TTestForm.FormCreate(Sender: TObject);
begin
  SecretForm.FSecretCode := 13;         { compiler stops with "Field identifier expected" }
end;
end.                                                                        { end of unit }

Although a program using the HideInfo unit can use objects of type TSecretForm, it cannot access the FSecretCode field in any of those objects.

Defining the component writer's interface

Declaring part of a class as protected makes that part visible only to the class itself and its descendants (and to other classes that share their unit files).

You can use protected declarations to define a component writer's interface to the class. Application units do not have access to the protected parts, but derived classes do. This means that component writers can change the way a class works without making the details visible to application developers.

Defining the runtime interface

Declaring part of a class as public makes that part visible to any code that has access to the class as a whole.

Public parts are available at runtime to all code, so the public parts of a class define its runtime interface. The runtime interface is useful for items that are not meaningful or appropriate at design time, such as properties that depend on runtime input or which are read-only. Methods that you intend for application developers to call must also be public.

Here is an example that shows two read-only properties declared as part of a component's runtime interface:

type
  TSampleComponent = class(TComponent)
  private
    FTempCelsius: Integer;                           { implementation details are private }
    function GetTempFahrenheit: Integer;
  public
    property TempCelsius: Integer read FTempCelsius;              { properties are public }
    property TempFahrenheit: Integer read GetTempFahrenheit;
  end;
...
function TSampleComponent.GetTempFahrenheit: Integer;
begin
  Result := FTempCelsius * 9 div 5 + 32;
end;

Defining the design-time interface

Declaring part of a class as published makes that part public and also generates runtime type information. Among other things, runtime type information allows the Object Inspector to access properties and events.

Because they show up in the Object Inspector, the published parts of a class define that class's design-time interface. The design-time interface should include any aspects of the class that an application developer might want to customize at design time, but must exclude any properties that depend on specific information about the runtime environment.

Read-only properties cannot be part of the design-time interface because the application developer cannot assign values to them directly. Read-only properties should therefore be public, rather than published.

Here is an example of a published property called Temperature. Because it is published, it appears in the Object Inspector at design time.

type
  TSampleComponent = class(TComponent)
  private
    FTemperature: Integer;                           { implementation details are private }
  published
    property Temperature: Integer read FTemperature write FTemperature;       { writable! }
  end;

Dispatching methods

Dispatch refers to the way a program determines where a method should be invoked when it encounters a method call. The code that calls a method looks like any other procedure or function call. But classes have different ways of dispatching methods.

The three types of method dispatch are

Static methods

All methods are static unless you specify otherwise when you declare them. Static methods work like regular procedures or functions. The compiler determines the exact address of the method and links the method at compile time.

The primary advantage of static methods is that dispatching them is very quick. Because the compiler can determine the exact address of the method, it links the method directly. Virtual and dynamic methods, by contrast, use indirect means to look up the address of their methods at runtime, which takes somewhat longer.

A static method does not change when inherited by a descendant class. If you declare a class that includes a static method, then derive a new class from it, the derived class shares exactly the same method at the same address. This means that you cannot override static methods; a static method always does exactly the same thing no matter what class it is called in. If you declare a method in a derived class with the same name as a static method in the ancestor class, the new method simply replaces the inherited one in the derived class.

An example of static methods

In the following code, the first component declares two static methods. The second declares two static methods with the same names that replace the methods inherited from the first component.

type
  TFirstComponent = class(TComponent)
    procedure Move;
    procedure Flash;
  end;

  TSecondComponent = class(TFirstComponent)
    procedure Move;       { different from the inherited method, despite same declaration }
    function Flash(HowOften: Integer): Integer;                  { this is also different }
  end;

Virtual methods

Virtual methods employ a more complicated, and more flexible, dispatch mechanism than static methods. A virtual method can be redefined in descendant classes, but still be called in the ancestor class. The address of a virtual method isn't determined at compile time; instead, the object where the method is defined looks up the address at runtime.

To make a method virtual, add the directive virtual after the method declaration. The virtual directive creates an entry in the object's virtual method table, or VMT, which holds the addresses of all the virtual methods in an object type.

When you derive a new class from an existing one, the new class gets its own VMT, which includes all the entries from the ancestor's VMT plus any additional virtual methods declared in the new class.

Overriding methods

Overriding a method means extending or refining it, rather than replacing it. A descendant class can override any of its inherited virtual methods.

To override a method in a descendant class, add the directive override to the end of the method declaration.

Overriding a method causes a compilation error if

The following code shows the declaration of two simple components. The first declares three methods, each with a different kind of dispatching. The other, derived from the first, replaces the static method and overrides the virtual methods.

type
  TFirstComponent = class(TCustomControl)
    procedure Move;                { static method }
    procedure Flash; virtual;      { virtual method }
    procedure Beep; dynamic;       { dynamic virtual method }
  end;

  TSecondComponent = class(TFirstComponent)
    procedure Move;                { declares new method }
    procedure Flash; override;     { overrides inherited method }
    procedure Beep; override;      { overrides inherited method }
  end;

Dynamic methods

Dynamic methods are virtual methods with a slightly different dispatch mechanism. Because dynamic methods don't have entries in the object's virtual method table, they can reduce the amount of memory that objects consume. However, dispatching dynamic methods is somewhat slower than dispatching regular virtual methods. If a method is called frequently, or if its execution is time-critical, you should probably declare it as virtual rather than dynamic.

Objects must store the addresses of their dynamic methods. But instead of receiving entries in the virtual method table, dynamic methods are listed separately. The dynamic method list contains entries only for methods introduced or overridden by a particular class. (The virtual method table, in contrast, includes all of the object's virtual methods, both inherited and introduced.) Inherited dynamic methods are dispatched by searching each ancestor's dynamic method list, working backwards through the inheritance tree.

To make a method dynamic, add the directive dynamic after the method declaration.

Abstract class members

When a method is declared as abstract in an ancestor class, you must surface it (by redeclaring and implementing it) in any descendant component before you can use the new component in applications. Delphi cannot create instances of a class that contains abstract members. For more information about surfacing inherited parts of classes, see "Creating properties," and "Creating methods."

Classes and pointers

Every class (and therefore every component) is really a pointer. The compiler automatically dereferences class pointers for you, so most of the time you do not need to think about this. The status of classes as pointers becomes important when you pass a class as a parameter. In general, you should pass classes by value rather than by reference. The reason is that classes are already pointers, which are references; passing a class by reference amounts to passing a reference to a reference.