Chapter 40
Creating a graphic component

A graphic control is a simple kind of component. Because a purely graphic control never receives focus, it does not have or need a window handle. Users can still manipulate the control with the mouse, but there is no keyboard interface.

The graphic component presented in this chapter is TShape, the shape component is on the Additional page of the Component palette. Although the component created is identical to the standard shape component, you need to call it something different to avoid duplicate identifiers. This chapter calls its shape component TSampleShape and shows you all the steps involved in creating the shape component:

Creating and registering the component

Creation of every component begins the same way: create a unit, derive a component class, register it, compile it, and install it on the Component palette. This process is outlined in "Creating a new component".

For this example, follow the general procedure for creating a component, with these specifics:

The resulting unit should look like this:

unit Shapes;
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms;
type
  TSampleShape = class(TGraphicControl)
  end;
procedure Register;
implementation
procedure Register;
begin
  RegisterComponent('Samples', [TSampleShape]);
end;
end.

Publishing inherited properties

Once you derive a component type, you can decide which of the properties and events declared in the protected parts of the ancestor class you want to surface in the new component. TGraphicControl already publishes all the properties that enable the component to function as a control, so all you need to publish is the ability to respond to mouse events and handle drag-and-drop.

Publishing inherited properties and events is explained in "Publishing inherited properties"and "Making events visible". Both processes involve redeclaring just the name of the properties in the published part of the class declaration.

For the shape control, you can publish the three mouse events, the three drag-and-drop events, and the two drag-and-drop properties:

type
  TSampleShape = class(TGraphicControl)
  published
    property DragCursor;        { drag-and-drop properties }
    property DragMode;
    property OnDragDrop;        { drag-and-drop events }
    property OnDragOver;
    property OnEndDrag;
    property OnMouseDown;       { mouse events }
    property OnMouseMove;
    property OnMouseUp;
  end;

The sample shape control now makes mouse and drag-and-drop interactions available to its users.

Adding graphic capabilities

Once you have declared your graphic component and published any inherited properties you want to make available, you can add the graphic capabilities that distinguish your component. You have two tasks to perform when creating a graphic control:

  1. Determining what to draw.
  2. Drawing the component image.

In addition, for the shape control example, you will add some properties that enable application developers to customize the appearance of the shape at design time.

Determining what to draw

A graphic control can change its appearance to reflect a dynamic condition, including user input. A graphic control that always looks the same should probably not be a component at all. If you want a static image, you can import the image instead of using a control.

In general, the appearance of a graphic control depends on some combination of its properties. The gauge control, for example, has properties that determine its shape and orientation and whether it shows its progress numerically as well as graphically. Similarly, the shape control has a property that determines what kind of shape it should draw.

To give your control a property that determines the shape it draws, add a property called Shape. This requires

  1. Declaring the property type.
  2. Declaring the property.
  3. Writing the implementation method.

Creating properties is explained in more detail in "Creating properties."

Declaring the property type

When you declare a property of a user-defined type, you must declare the type first, before the class that includes the property. The most common sort of user-defined type for properties is enumerated.

For the shape control, you need an enumerated type with an element for each kind of shape the control can draw.

Add the following type definition above the shape control class's declaration.

type
  TSampleShapeType = (sstRectangle, sstSquare, sstRoundRect, sstRoundSquare,
    sstEllipse, sstCircle);
  TSampleShape = class(TGraphicControl) { this is already there }

You can now use this type to declare a new property in the class.

Declaring the property

When you declare a property, you usually need to declare a private field to store the data for the property, then specify methods for reading and writing the property value. Often, you don't need to use a method to read the value, but can just point to the stored data instead.

For the shape control, you will declare a field that holds the current shape, then declare a property that reads that field and writes to it through a method call.

Add the following declarations to TSampleShape:

type
  TSampleShape = class(TGraphicControl)
  private
    FShape: TSampleShapeType;  { field to hold property value }
    procedure SetShape(Value: TSampleShapeType);
  published
    property Shape: TSampleShapeType read FShape write SetShape;
  end;

Now all that remains is to add the implementation of SetShape.

Writing the implementation method

When the read or write part of a property definition uses a method instead of directly accessing the stored property data, you need to implement the method.

Add the implementation of the SetShape method to the implementation part of the unit:

procedure TSampleShape.SetShape(Value: TSampleShapeType);
begin
  if FShape <> Value then                                { ignore if this isn't a change }
  begin
    FShape := Value;                                               { store the new value }
    Invalidate;                                     { force a repaint with the new shape }
  end;
end;

Overriding the constructor and destructor

To change default property values and initialize owned classes for your component, you must override the inherited constructor and destructor. In both cases, remember always to call the inherited method in your new constructor or destructor.

Changing default property values

The default size of a graphic control is fairly small, so you can change the width and height in the constructor. Changing default property values is explained in more detail in "Modifying an existing component.".

In this example, the shape control sets its size to a square 65 pixels on each side.

Add the overridden constructor to the declaration of the component class:

type
  TSampleShape = class(TGraphicControl)
  public                                                { constructors are always public }
    constructor Create(AOwner: TComponent); override       { remember override directive }
  end;

  1. Redeclare the Height and Width properties with their new default values:
    type
      TSampleShape = class(TGraphicControl)
      ...
      published
        property Height default 65;
        property Width default 65;
      end;
    
  2. Write the new constructor in the implementation part of the unit:
    constructor TSampleShape.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);  { always call the inherited constructor }
      Width := 65;
      Height := 65;
    end;
    

Publishing the pen and brush

By default, a canvas has a thin black pen and a solid white brush. To let developers change the pen and brush, you must provide classes for them to manipulate at design time, then copy the classes into the canvas during painting. Classes such as an auxiliary pen or brush are called owned classes because the component owns them and is responsible for creating and destroying them.

Managing owned classes requires

  1. Declaring the class fields.
  2. Declaring the access properties.
  3. Initializing owned classes.
  4. Setting owned classes' properties.

Declaring the class fields

Each class a component owns must have a class field declared for it in the component. The class field ensures that the component always has a pointer to the owned object so that it can destroy the class before destroying itself. In general, a component initializes owned objects in its constructor and destroys them in its destructor.

Fields for owned objects are nearly always declared as private. If applications (or other components) need access to the owned objects, you can declare published or public properties for this purpose.

Add fields for a pen and brush to the shape control:

type
  TSampleShape = class(TGraphicControl)
  private            { fields are nearly always private }
    FPen: TPen;      { a field for the pen object }
    FBrush: TBrush;  { a field for the brush object }
    ...
  end;

Declaring the access properties

You can provide access to the owned objects of a component by declaring properties of the type of the objects. That gives developers a way to access the objects at design time or runtime. Usually, the read part of the property just references the class field, but the write part calls a method that enables the component to react to changes in the owned object.

To the shape control, add properties that provide access to the pen and brush fields. You will also declare methods for reacting to changes to the pen or brush.

type
  TSampleShape = class(TGraphicControl)
  ...
  private                                              { these methods should be private }
    procedure SetBrush(Value: TBrush);
    procedure SetPen(Value: TPen);
  published                                        { make these available at design time }
    property Brush: TBrush read FBrush write SetBrush;
    property Pen: TPen read FPen write SetPen;
  end;

Then, write the SetBrush and SetPen methods in the implementation part of the unit:

procedure TSampleShape.SetBrush(Value: TBrush);
begin
  FBrush.Assign(Value);                          { replace existing brush with parameter }
end;

procedure TSampleShape.SetPen(Value: TPen);
begin
  FPen.Assign(Value);                              { replace existing pen with parameter }
end;

To directly assign the contents of Value to FBrush...

  FBrush := Value;

...would overwrite the internal pointer for FBrush, lose memory, and create a number of ownership problems.

Initializing owned classes

If you add classes to your component, the component's constructor must initialize them so that the user can interact with the objects at runtime. Similarly, the component's destructor must also destroy the owned objects before destroying the component itself.

Because you have added a pen and a brush to the shape control, you need to initialize them in the shape control's constructor and destroy them in the control's destructor:

  1. Construct the pen and brush in the shape control constructor:
    constructor TSampleShape.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);                      { always call the inherited constructor }
      Width := 65;
      Height := 65;
      FPen := TPen.Create;                                               { construct the pen }
      FBrush := TBrush.Create;                                         { construct the brush }
    end;
    
  2. Add the overridden destructor to the declaration of the component class:
    type
      TSampleShape = class(TGraphicControl)
      public                                                  { destructors are always public}
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;                          { remember override directive }
      end;
    
  3. Write the new destructor in the implementation part of the unit:
    destructor TSampleShape.Destroy;
    begin
      FPen.Free;                                                    { destroy the pen object }
      FBrush.Free;                                                { destroy the brush object }
      inherited Destroy;                         { always call the inherited destructor, too }
    end;
    

Setting owned classes' properties

As the final step in handling the pen and brush classes, you need to make sure that changes in the pen and brush cause the shape control to repaint itself. Both pen and brush classes have OnChange events, so you can create a method in the shape control and point both OnChange events to it.

Add the following method to the shape control, and update the component's constructor to set the pen and brush events to the new method:

type
  TSampleShape = class(TGraphicControl)
  published
    procedure StyleChanged(Sender: TObject);
  end;
...
implementation
...
constructor TSampleShape.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);                      { always call the inherited constructor }
  Width := 65;
  Height := 65;
  FPen := TPen.Create;                                               { construct the pen }
  FPen.OnChange := StyleChanged;                       { assign method to OnChange event }
  FBrush := TBrush.Create;                                         { construct the brush }
  FBrush.OnChange := StyleChanged;                     { assign method to OnChange event }
end;

procedure TSampleShape.StyleChanged(Sender: TObject);
begin
  Invalidate(True);                                    { erase and repaint the component }
end;

With these changes, the component redraws to reflect changes to either the pen or the brush.

Drawing the component image

The essential element of a graphic control is the way it paints its image on the screen. The abstract type TGraphicControl defines a method called Paint that you override to paint the image you want on your control.

The Paint method for the shape control needs to do several things:

Overriding the Paint method requires two steps:

  1. Add Paint to the component's declaration.
  2. Write the Paint method in the implementation part of the unit.

For the shape control, add the following declaration to the class declaration:

type
  TSampleShape = class(TGraphicControl)
  ...
  protected
    procedure Paint; override;
  ...
  end;

Then write the method in the implementation part of the unit:

procedure TSampleShape.Paint;
begin
  with Canvas do
  begin
    Pen := FPen;                                              { copy the component's pen }
    Brush := FBrush;                                        { copy the component's brush }
    case FShape of
      sstRectangle, sstSquare:
        Rectangle(0, 0, Width, Height);                    { draw rectangles and squares }
      sstRoundRect, sstRoundSquare:
        RoundRect(0, 0, Width, Height, Width div 4, Height div 4); { draw rounded shapes }
      sstCircle, sstEllipse:
        Ellipse(0, 0, Width, Height);                                { draw round shapes }
    end;
  end;
end;

Paint is called whenever the control needs to update its image. Windows tells controls to paint when they first appear or when a window in front of them goes away. In addition, you can force repainting by calling Invalidate, as the StyleChanged method does.

Refining the shape drawing

The standard shape control does one more thing that your sample shape control does not yet do: it handles squares and circles as well as rectangles and ellipses. To do that, you need to write code that finds the shortest side and centers the image.

Here is a refined Paint method that adjusts for squares and ellipses:

procedure TSampleShape.Paint;
var
  X, Y, W, H, S: Integer;
begin
  with Canvas do
  begin
    Pen := FPen;                                              { copy the component's pen }
    Brush := FBrush;                                        { copy the component's brush }
    W := Width;                                                { use the component width }
    H := Height;                                              { use the component height }
    if W < H then S := W else S := H;                { save smallest for circles/squares }

    case FShape of                                   { adjust height, width and position }
      sstRectangle, sstRoundRect, sstEllipse:
        begin
          X := 0;                                  { origin is top-left for these shapes }
          Y := 0;
        end;
      sstSquare, sstRoundSquare, sstCircle:
        begin
          X := (W - S) div 2;                             { center these horizontally... }
          Y := (H - S) div 2;                                        { ...and vertically }
          W := S;                                  { use shortest dimension for width... }
          H := S;                                                    { ...and for height }
        end;
    end;

    case FShape of
      sstRectangle, sstSquare:
        Rectangle(X, Y, X + W, Y + H);                     { draw rectangles and squares }
      sstRoundRect, sstRoundSquare:
        RoundRect(X, Y, X + W, Y + H, S div 4, S div 4);           { draw rounded shapes }
      sstCircle, sstEllipse:
        Ellipse(X, Y, X + W, Y + H);                                 { draw round shapes }
    end;
  end;
end;