Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents

Part I - Foundations
  Chapter 1 – Delphi 7 and Its IDE
  Chapter 2 – The Delphi Programming Language
  Chapter 3 – The Run-Time Library
  Chapter 4 – Core Library classes
  Chapter 5 – Visual Controls
  Chapter 6 – Building the User Interface
  Chapter 7 – Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 – The Architecture of Delphi Applications
  Chapter 9 – Writing Delphi Components
  Chapter 10 – Libraries and Packages
  Chapter 11 – Modeling and OOP Programming (with ModelMaker)
  Chapter 12 – From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 – Delphi's Database Architecture
  Chapter 14 – Client/Server with dbExpress
  Chapter 15 – Working with ADO
  Chapter 16 – Multitier DataSnap Applications
  Chapter 17 – Writing Database Components
  Chapter 18 – Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 – Internet Programming: Sockets and Indy
  Chapter 20 – Web Programming with WebBroker and WebSnap
  Chapter 21 – Web Programming with IntraWeb
  Chapter 22 – Using XML Technologies
  Chapter 23 – Web Services and SOAP
  Chapter 24 – The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 – Delphi for .NET Preview: The Language and the RTL
       
  Appendix A – Extra Delphi Tools by the Author
  Appendix B – Extra Delphi Tools from Other Sources
  Appendix C – Free Companion Books on Delphi
       
  Index    
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

 
Previous Section Next Section

A Complex Graphical Component

In this section, I'll demonstrate how to build a graphical Arrow component. You can use such a component to indicate a flow of information or an action. This component is quite complex, so I'll show you the various steps instead of looking directly at the complete source code. The component I've added to the MdPack package is the final version of this process, which demonstrates several important concepts:

  • The definition of new enumerated properties, based on custom enumerated data types.

  • The implementation of the component's Paint method, which provides its user interface and should be generic enough to accommodate all the possible values of the various properties, including its Width and Height. The Paint method plays a substantial role in this graphical component.

  • The use of properties of TPersistent-derived classes, such as TPen and TBrush, as well as the issues related to their creation and destruction and to handling their OnChange events internally in your component.

  • The definition of a custom event handler for the component, which responds to user input (in this case, a double-click on the point of the arrow). This requires direct handling of Windows messages and the use of the Windows API for graphic regions.

  • The registration of properties in Object Inspector categories and the definition of a custom category.

Defining an Enumerated Property

After generating the new component with the Component Wizard and choosing TGraphicControl as the parent class, you can begin to customize the component. The arrow can point in any of four directions: up, down, left, or right. An enumerated type expresses these choices:

type
  TMdArrowDir = (adUp, adRight, adDown, adLeft);

This enumerated type defines a private data member of the component, a parameter of the procedure used to change it, and the type of the corresponding property.

The ArrowHeight property determines the size of the arrowhead, and the Filled property specifies whether to fill the arrowhead with color:

type
  TMdArrow = class (TGraphicControl)
  private
    FDirection: TMdArrowDir;
    FArrowHeight: Integer;
    FFilled: Boolean;
    procedure SetDirection (Value: TMd4ArrowDir);
    procedure SetArrowHeight (Value: Integer);
    procedure SetFilled (Value: Boolean);
  published
    property Width default 50;
    property Height default 20;
    property Direction: TMd4ArrowDir
      read FDirection write SetDirection default adRight;
    property ArrowHeight: Integer
      read FArrowHeight write SetArrowHeight default 10;
    property Filled: Boolean read FFilled write SetFilled default False;
Note 

A graphic control has no default size, so when you place it in a form, its size will be a single pixel. For this reason, it is important to add a default value for the Width and Height properties and set the class fields to the default property values in the class constructor.

The three custom properties are read directly from the corresponding field and are written using three Set methods, all having the same standard structure:

procedure TMdArrow.SetDirection (Value: TMdArrowDir);
begin
  if FDirection <> Value then
  begin
    FDirection := Value;
    ComputePoints;
    Invalidate;
  end;
end;

Notice that you ask the system to repaint the component (by calling Invalidate) only if the property is really changing its value and after calling the ComputePoints method, which computes the triangle delimiting the arrowhead. Otherwise, the code is skipped and the method ends immediately. This code structure is common, and you will use it for most of the Set procedures of properties.

You must also remember to set the properties' default values in the component's constructor:

constructor TMdArrow.Create (AOwner: TComponent);
begin
  // call the parent constructor
  inherited Create (AOwner);
  // set the default values
  FDirection := adRight;
  Width := 50;
  Height := 20;
  FArrowHeight := 10;
  FFilled := False;

As mentioned before, the default value specified in the property declaration is used only to determine whether to save the property's value to disk. The Create constructor is defined in the public section of the new component's type definition, and the constructor is marked by the override keyword, as it replaces the virtual Create constructor of TComponent. It is fundamental to remember the override specifier; otherwise, when Delphi creates a new component of this class, it will call the base class's constructor, rather than the one you've written for your derived class.

Writing the Paint Method

Drawing the arrow in the various directions and with the various styles requires a fair amount of code. To perform custom painting, you override the Paint method and use the protected Canvas property.

Instead of computing the position of the arrowhead points in drawing code that will be executed often, I've written a separate function to compute the arrowhead area and store it in an array of points defined among the private fields of the component:

FArrowPoints: array [0..3] of TPoint;

These points are determined by the ComputePoints private method, which is called every time a component property changes. Here is an excerpt of its code:

procedure TMdArrow.ComputePoints;
var
  XCenter, YCenter: Integer;
begin
  // compute the points of the arrowhead
  YCenter := (Height - 1) div 2;
  XCenter := (Width - 1) div 2;
  case FDirection of
    adUp: begin
      FArrowPoints [0] := Point (0, FArrowHeight);
      FArrowPoints [1] := Point (XCenter, 0);
      FArrowPoints [2] := Point (Width-1, FArrowHeight);
    end;
  // and so on for the other directions

The code computes the center of the component area (dividing the Height and Width properties by two) and then uses the center to determine the position of the arrowhead. In addition to changing the direction or other properties, you need to refresh the position of the arrowhead when the size of the component changes. You can override the SetBounds method of the component, which is called by VCL every time the Left, Top, Width, and Height properties of a component change:

procedure TMdArrow.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
begin
  inherited SetBounds (ALeft, ATop, AWidth, AHeight);
  ComputePoints;
end;

Once the component knows the position of the arrowhead, its painting code becomes simpler. Here is an excerpt of the Paint method:

procedure TMdArrow.Paint;
var
  XCenter, YCenter: Integer;
begin
  // compute the center
  YCenter := (Height - 1) div 2;
  XCenter := (Width - 1) div 2;
   
  // draw the arrow line
  case FDirection of
    adUp: begin
      Canvas.MoveTo (XCenter, Height-1);
      Canvas.LineTo (XCenter, FArrowHeight);
    end;
    // and so on for the other directions
  end;
   
  // draw the arrow point, eventually filling it
  if FFilled then
    Canvas.Polygon (FArrowPoints)
  else
    Canvas.PolyLine (FArrowPoints);
end;

You can see an example of the output of this component in Figure 9.6.

Click To expand
Figure 9.6: The output of the Arrow component

Adding TPersistent Properties

To make the output of the component more flexible, I've added to it two new properties, Pen and Brush, defined with a class type (a TPersistent data type, which defines objects that Delphi can automatically stream). These properties are a little more complex to handle, because the component now has to create and destroy these internal objects. This time, however, you also export the internal objects using properties, so that users can directly change these internal objects from the Object Inspector. To update the component when these subobjects change, you'll also need to handle their internal OnChange property. Here is the definition of the Pen property and the other changes to the definition of the component class (the code for the Brush property is similar):

type
  TMdArrow = class (TGraphicControl)
  private
    FPen: TPen;
    ...
    procedure SetPen (Value: TPen);
    procedure RepaintRequest (Sender: TObject);
  published
    property Pen: TPen read FPen write SetPen;
  end;

You first create the object in the constructor and set its OnChange event handler:

constructor TMdArrow.Create (AOwner: TComponent);
begin
  ...
  // create the pen and the brush
  FPen := TPen.Create;
  // set a handler for the OnChange event
  FPen.OnChange := RepaintRequest;
end;

These OnChange events are fired when one of the properties of the pen changes; all you have to do is to ask the system to repaint your component:

procedure TMdArrow.RepaintRequest (Sender: TObject);
begin
  Invalidate;
end;

You must also add a destructor to the component, to remove the graphical object from memory (and free its system resources). All the destructor has to do is call the Pen object's Free method.

A property related to persistent objects requires special handling: Instead of copying the pointer to the object, you have to copy the internal data of the object passed as a parameter. The standard := operation copies the pointer, so in this case you have to use the Assign method:

procedureTMdArrow.SetPen (Value: TPen);
begin
  FPen.Assign(Value);
  Invalidate;
end;

Many TPersistent classes have an Assign method you should use when you need to update the data of these objects. Now, to use the pen for the drawing, you must modify the Paint method, setting the corresponding property of the component Canvas to the value of the internal object before drawing a line (see the example of the component's new output in Figure 9.7):

Click To expand
Figure 9.7: The output of the Arrow component with a thick pen and a special hatch brush
procedure TMdArrow.Paint;
begin
  // use the current pen
  Canvas.Pen := FPen;

As the Canvas uses a setter routine to Assign the pen object, you're not simply storing a reference to the pen in a field of the Canvas, but you are copying all of its data. This means that you can freely destroy the local Pen object (FPen) and that modifying FPen won't affect the canvas until Paint is called and the code above is executed again.

Defining a Custom Event

To complete the development of the Arrow component, let's add a custom event. Most of the time, new components use the events of their parent classes. For example, in this component, I've made some standard events available by redeclaring them in the published section of the class:

type
  TMdArrow = class (TGraphicControl)
  published
    property OnClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;

Thanks to this declaration, the events (originally declared in a parent class) will be available in the Object Inspector when the component is installed.

Sometimes, however, a component requires a custom event. To define a new event, you first need to ensure that there is already a method pointer type suitable for use by the event; if not, you need to define a new event type. This type is a method pointer type (see Chapter 5, "Visual Controls," for details). In both cases, you need to add to the class a field of the event's type: here is the definition I've added in the private section of the TMdArrow class:

FArrowDblClick: TNotifyEvent;

I've used the TNotifyEvent type, which has only a Sender parameter and is used by Delphi for many events, including OnClick and OnDblClick events. Using this field I've defined a published property, with direct access to the field:

property OnArrowDblClick: TNotifyEvent
  read FArrowDblClick write FArrowDblClick;

(Notice again the standard naming convention, with event names starting with On.) The fArrowDblClick method pointer is activated (executing the corresponding function) inside the specific ArrowDblClick dynamic method. This happens only if an event handler has been specified in the program that uses the component:

procedure TMdArrow.ArrowDblClick;
begin
  if Assigned (FArrowDblClick) then
    FArrowDblClick (Self);
end;
Tip 

The use of Self as parameter of the invocation of the event handler method ensures that when the method is called its Sender parameter would actually refer to the object that fired the event, which you generally expect as a component user.

Using Low-Level Windows API Calls

The fArrowDblClick method is defined in the protected section of the type definition to allow future descendant classes to both call and change it. Basically, this method is called by the handler of the
wm_LButtonDblClk Windows message, but only if the double-click took place inside the arrow's point. To test this condition, you can use some of the Windows API's region functions.

Note 

A region is a screen area enclosed by any shape. For example, you can build a polygonal region using the three vertices of the arrow-point triangle. The only problem is that to fill the surface properly, you must define an array of TPoints in a clockwise direction (see the description of the CreatePolygonalRgn in the Windows API Help for the details of this strange approach). That's what I did in the ComputePoints method.

Once you have defined a region, you can use the PtInRegion API call to test whether the point where the double-click occurred is inside the region. The complete source code for this procedure is as follows:

procedure TMdArrow.WMLButtonDblClk (
  var Msg: TWMLButtonDblClk); // message wm_LButtonDblClk;
var
  HRegion: HRgn;
begin
  // perform default handling
  inherited;
   
  // compute the arrowhead region
  HRegion := CreatePolygonRgn (FArrowPoints, 3, WINDING);
  try  // check whether the click took place in the region
    if PtInRegion (HRegion, Msg.XPos, Msg.YPos) then
      ArrowDblClick;
  finally
    DeleteObject (HRegion);
  end;
end;

The CLX Version: Calling Qt Native Functions

The previous code won't be portable to Linux and makes no sense for the CLX/Qt version of the component. If you want to build a similar component for the CLX class library, you can replace the Win32 API calls with direct (low-level) calls to the Qt layer, creating an object of the QRegion class, as in the following listing:

procedure TMdArrow.DblClick;
var
  HRegion: QRegionH;
  MousePoint: TPoint;
begin
  // perform default handling
  inherited;
  // compute the arrow head region
  HRegion := QRegion_create (PPointArray(FArrowPoints), True);
  try
    // get the current mouse position
    GetCursorPos (MousePoint);
    MousePoint := ScreenToClient(MousePoint);
    // check whether the click took place in the region
    if QRegion_contains(HRegion, PPoint(@MousePoint)) then
      ArrowDblClick;
  finally
    QRegion_destroy(HRegion);
  end;
end;

Registering Property Categories

You've added to this component some custom properties and a new event. If you arrange the properties in the Object Inspector by category, all the new elements will appear in the generic Miscellaneous category. Of course, this is far from ideal, but you can easily register the new properties in one of the available categories.

You can register a property (or an event) in a category by calling one of the four overloaded versions of the RegisterPropertyInCategory function, defined in the DesignIntf unit. When calling this function, you indicate the name of the category, and you can specify the property name, its type, or the property name and the component it belongs to. For example, you can add the following lines to the Register procedure of the unit to register the OnArrowDblClick event in the Input category and the Filled property in the Visual category:

uses
  DesignIntf;
   
procedure Register;
begin
  RegisterPropertyInCategory ('Input', TMdArrow, 'OnArrowDblClick');
  RegisterPropertyInCategory ('Visual', TMdArrow, 'Filled');
end;

The first parameter is a string indicating the category name—a much simpler solution than the original Delphi 5 approach of using category classes. You can define a new category in a straightforward manner by passing its name as the first parameter of the RegisterPropertyInCategory function:

RegisterPropertyInCategory ('Arrow', TMdArrow, 'Direction');
RegisterPropertyInCategory ('Arrow', TMdArrow, 'ArrowHeight');

Creating a new category for the specific properties of your component can make it much simpler for a user to locate its specific features. Notice, though, that because you rely on the DesignIntf unit, you should compile the unit containing these registrations in a design-time package, not a run-time package (the required DesignIde unit cannot be distributed). For this reason, I've written this code in a separate unit from the one defining the component and added the new unit (MdArrReg) to the package MdDesPk, including all the design-time-only units; this approach is discussed later, in the section "Installing the Property Editor."

Warning 

It's debatable whether using a category for the specific properties of a component is a good idea. On one hand, a user of the component can easily spot specific properties. At the same time, some of the new properties may not pertain to any of the existing categories. On the other hand, categories can be overused. If every component introduces new categories, users may be confused. You also face the risk of having as many categories as there are properties.

Notice that my code registers the Filled property in two different categories. This is not a problem, because the same property can show up multiple times in the Object Inspector under different groups, as you can see in Figure 9.8. To test the arrow component, I've written the ArrowDemo program, which allows you to modify most of its properties at run time. This type of test is important after you have written a component or while you are writing it.


Figure 9.8:  The Arrow component defines a custom property category, Arrow, as you can see in the Object Inspector. Notice that properties can be visible in multiple section, such as the Filled property in this case.
Note 

The Localizable property category has a special role related to the use of the ITE (Integrated Translation Environment). When a property is part of this category, its value is listed in the Translation Environment as a property that can be translated into another language.


 
Previous Section Next Section


 


 

Delphi Sources


Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide
ร๐๓๏๏เ ยส๎ํ๒เ๊๒ๅ   Facebook   ั๑๛๋๊เ ํเ Twitter