Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Database

Writing Delphi Components


Visual Tools 96: Writing Delphi Components

Writing Delphi Components

A multicharacter listbox shows you how

William Stamatakis

Bill is an independent computer consultant. He can be reached at [email protected].


Delphi is a component-based, visual-programming environment designed to simplify Windows programming by letting you drag components from the palette within the Delphi IDE and drop them onto a form. This action creates a data member of the selected component's type. Component users can then manipulate the object by setting properties within the Object Inspector at design time, or directly through programming.

All components are contained within a class hierarchy called the Visual Component Library (VCL), an application framework that encapsulates the Windows API and makes Windows development less cryptic and more expeditious. In a nutshell, VCL is to Delphi what OWL is to Borland C++, or MFC is to Visual C++. In this article, I'll show you how to create a component that can be used in any Delphi application.

The Visual Component Library

Delphi 2.0's VCL ships with more than 90 components, categorized in a tabbed-component palette in the IDE (see Figure 1). These controls include edit, listbox, tree views, notebook tabs, grids, multimedia, databases, and others. You can also add third-party components, including VBX and OCX custom controls, to the palette. Furthermore, you can create customized component libraries. The VCL contains utilities such as I/O stream and collection classes that save hours of development time.

At the root of the class hierarchy is TObject. All classes are ultimately derived from TObject; in other words, it is impossible to create a new class that is not a descendent of one of the classes in the VCL. Therefore, by creating a new class, you are extending the VCL. However, when creating a Delphi component, you have several options: You can create an original, graphic, or nonvisual control. Furthermore, you can modify existing controls, or subclass a Windows control.

Creating an original control. You derive your component from TWinControl. A standard control is a window; therefore, it has a window handle, which means Windows is aware of its existence and is capable of receiving keyboard focus. A valid window handle must be passed to API functions that require a handle to apply the function task to the correct window. TWinControl encapsulates the tedious code necessary to create a standard control, so by deriving your new component from it, you have in effect already created a standard control to which you can add new enhancements.

Creating a graphic control. If you want to create a control that does not need to receive keyboard focus, then you should derive your component from TGraphicControl. A descendent of this class has no need for a window handle because there is no keyboard focus. Consequently, if a control has no window handle, then Windows is not cognizant of its existence, so it is not consuming any system resources. Although there is no keyboard interface, you can still manipulate an object of graphic-control type via the mouse interface. TGraphicControl is an abstract class that inherits most of its functionality from TControl, but in addition, encapsulates the GDI functions located in the Windows API into a Canvas. Therefore, instead of making direct GDI calls (which is still an option in Delphi) that require you to first have a valid device-context handle that must be released after your final GDI function call, you could use the Canvas property, which has its own properties that represent the current brush, pen, and font. What's more, the Canvas also handles the operation of retrieving and releasing device-context handles as needed. In addition, TGraphicControl receives a WM_PAINT message every time it is necessary for your custom graphic control to be repainted. Hence, to provide your custom graphic control with the ability to paint its image, you must override the virtual Paint method defined in TGraphicControl.

Creating a nonvisual control. When you want to create a component that is visible at design time, but invisible at run time (that is, a nonvisual component), derive your custom component from TComponent. TComponent is an abstract base class that defines the fundamental behavior that a component must have to participate in the Form Designer, such as appearing on the component palette and interacting with the Object Inspector. Most productivity tool components such as ListBox, TEdit, and TButton are visual components because they are visible both at design time and run time. Nevertheless, sometimes it is preferable to implement nonvisual components. For instance, you may want to expose API calls by providing a visual interface to underlying cumbersome functions that may have parameters that must have previously attained valid values by calling different functions. Delphi database components are examples of this. Most RDBMSs require you to connect successfully before performing any action on the data that resides in them. Therefore, most database API calls require a valid database handle that identifies a specific session as result of a successful login. Delphi database components have effectively encapsulated the task of managing database handles, and have exposed other parameters of database API functions in the form of properties, events, and methods.

Modifying an existing control. This is the quickest way to create a new component. The three most-common reasons for modifying an existing component are to add new properties, remove existing ones, and change default property values. At a minimum, you must derive your new class from an existing one. Removing properties is difficult because when you derive from a base class, you inherit everything. However, Delphi provides an alternative approach. There are components in the VCL that have the word "Custom" as part of their name (TCustomComboBox, for instance). These components have most of their properties declared in the protected section of the component declaration. As a result, when you derive your new class from one of these, the properties are not in the published section, so they will not be accessible to an object of your new component type. To provide access to properties, you selectively redeclare the properties in the published section of your new class declaration. Finally, to change a default property value, you simply redeclare an existing property from your base class (in the published section of your new class declaration), followed by the keyword default and the new value. You must also explicitly set the default value by overriding the constructor of the new class, and initialize the property within the constructor.

Subclassing Windows controls. This technique requires you to have more knowledge of Windows internals. Windows has its own concept of a window class that is similar to an object-oriented language's class. A window is created based on a window class. The window class must be registered before a window can be created based on it. A window class defines the window procedure that processes messages and some other essential characteristics that a window created from this class must possess. The window procedure is the core mechanism of every window that determines its behavior based on messages (or events). Subclassing is the act of intercepting specific messages in a window and producing the behavior that you desire based on those messages. Basically, you create your own window procedure and replace the one determined by the window class. Then, all messages will go to your window procedure. Next, capture the messages that you want to perform custom routines on and make sure to call the original window procedure for all other messages. Delphi lets you create a component wrapper around a window class by encapsulating this sort of complicated task and allowing you to create event handlers to subclass windows controls. You can then derive new components from this component wrapper.

Component Creation in Delphi

Though there are few constraints for creating Delphi components, they should contain properties, events, and methods. When the component has been created, it then must be registered.

Properties. A property is an interface by which the characteristics of a component may be revealed and manipulated. Properties are accessible at design time. To view or change a property value for a component, click on the designated component in the Form Designer and invoke the Object Inspector. A tabbed dialog will appear containing the Properties and Event tabs. By setting property values via the Object Inspector, you are providing the values to initialize attributes for an object of that type component that will be instantiated at run time without writing a line of code. Although it may not be obvious, properties provide the same functionality as class constructors; however, the properties interface tends to be more intuitive, and makes initializing an object's attributes effortless. Properties can also be set in code; for example, edit1.text= 'hello' will cause an edit control to display "hello" in the client area. This action is similar to that of a component member function/method. To create a property, you must declare it in the published level of your component declaration. Next, you must declare a data member indicating where the property will be stored. Finally, you must decide if the property value will directly or indirectly set and/or get the underlying data that is stored in the corresponding data member.

Events. An event is an occurrence of a specific action. More specifically, a Windows event is a message generated by Windows or a Windows process/application. As the component writer, you have the option of having your component trap messages internally, or you can expose the realization of a particular event by creating a method pointer (a pointer to a specific event handler of a specific object instance). It is the component writer's responsibility to ensure that the method pointer is assigned the address of an event handler. The event handler is the function called when a certain event occurs. The body of the event-handler function should contain the code that the end user wants to be executed as a result of a specific event.

Methods. A method is a member function/procedure of a component. A method is another mechanism for interfacing with an object instance of a given component. Theoretically, when you call a method, you are sending a message to an object; the object then performs some action. Therefore, a method causes the object to manifest certain behaviors. Methods are implemented just as member functions are in a class. Hence, methods are not properties, so there is no visual interface at design time. They are, therefore, only accessible at run time. Methods can reside in all access levels of components, except for the published level.

Registration. The main reason you want to create a component is so that the end user can drag-and-drop it from the component palette onto a form and visually manipulate it by setting property values and creating event handlers. For this to be possible, the component must be registered. Registering a component adds it to the component palette, and determines the inherent store- and-load mechanism by which to make a component persistent. Unless the component writer alters them, component property settings are automatically stored in a form (.DFM) file which is eventually attached to the compiled application. At run time, the property settings are automatically loaded back into the component.

Creating a Listbox Component

Experienced Windows developers know that the standard Windows listbox only supports first-character searches: If you press the letter "a," the highlight will move to the first occurrence of the item that starts with "a." If you then press "p," the highlight will not attempt to search for an item that starts with "ap," but would move to the first occurrence of an item that starts with "p," if one exists. The listbox component I'll create here lets you perform a multicharacter search.

When you begin typing in my listbox, a search dialog pops up with the characters that you have typed. At that point, you can click on the Find Next button or press Enter. Both will have the same effect; the highlight will move to an item that starts with that string of characters. If the string is not found, a message will appear, stating this. Note that the highlight does not move in a multiselect listbox, but the index of the appropriate item is returned.

To create a component, you could write all the code from scratch, or you could use the Component Expert to get started. In fact, you can only run the expert the first time you are generating a component. If you want to make changes, such as derive from a different base class, you'll have to code from scratch anyway. Therefore, I will show you how to create my listbox component completely from a programming perspective.

Listing One presents the Pascal source code for the multicharacter listbox component. (The complete source code and related files are available electronically.) First, I derive my component TBillListBox from the Delphi TListBox. Since TListBox is defined in the StdCtrls unit, I have included it in the uses clause in the interface section. Also, I've added an Autosearch property that allows you to specify at design time whether the search mechanism should be used. Setting Autosearch to True (the default is False) enables the search mechanism. Since the published section of a component is where you define properties and events, Autosearch is declared in the published section as Boolean.

Next, I subclass the KeyPress event handler of TListBox and override its behavior. When the end user presses a key, this KeyPress event is called first, and checks to see if Autosearch is set to True. If it is, I disallow the default KeyPress behavior and invoke my multicharacter item-search mechanism.

When the search mechanism is called, I open a search dialog (see Listing Two). To get the data from the KeyPress event into the edit control of the search dialog and perform a search, I pass a reference to the component as a parameter to the search dialog's create constructor. The create constructor, by default, will accept any TObject-derived object as a parameter. All members of the search dialog class have access to the Owner variable, which stores the object passed through the search dialog's create constructor.

To activate the search through the search-dialog object, the Find Next button must be clicked. This will cause the click event handler to be called. Within the click event handler, the SearchList method is called by type casting Owner as TBillListBox. SearchList provides the multicharacter search functionality by calling the SendMessage Windows API function. SendMessage returns either the list index of the item if found, or a LB_ERR if not found. Handle is the hWnd of the listbox component. LB_FINDSTRING is the message sent to the listbox indicating the function to be performed. CurrentIndex stores the starting index in the listbox to search forward from. LongInt(item) is a typecast of the string address that contains the string to search for in the listbox.

Property Editors

Delphi component properties all have default property editors assigned to them based on their data type. For example, if a component contains a property of an enumerated data type, it will, by default, be represented in the Object Inspector as a drop-down list filled with all possible enumerations. However, as the component developer, you have the capability to override the default property editor and replace it with a completely different interface, such as a modal dialog box. To make this listbox component more interesting, I replaced the Items property editor with a somewhat more intuitive version. When the programmer clicks on the Items property in the Object Inspector, a custom List Editor dialog appears.

To implement this feature, I derived my new property editor from an existing property-editor class and overrode some of the key methods that control its behavior. The Edit method controls how data is accessed and modified through the Object Inspector. The GetAttributes method controls the appearance of the property field in the Object Inspector, including display type and read/write capabilities, and the property's value field.

In the case of my TBillListBox component, the Items property is only accessible through a dialog interface, by setting the Item property attributes to paDialog and paReadonly; see Listing One. The GetValue method returns a string representing the current value of the property into property's value field. GetValue returns unknown by default; therefore, I overrode it to return the "Click to add/modify items...".

To put my new property-editor class into effect, I associate it with a specific data type by calling RegisterPropertyEditor. Now, the component can be registered in the component palette in a specific tab; in this case, I registered my listbox in the Samples tab of the component palette.

Conclusion

Delphi components extend the base programming language, thereby promoting the concept of reusable object code. The techniques presented here should get you started in creating custom components. For more information, you can also refer to "Visually Constructing Delphi Components," by Al Williams (Dr. Dobb's Journal, December 1995).

Figure 1: The Delphi IDE.

Listing One


{ * Unit Name: tbillist.pas
  * Author:    William Stamatakis
  * Note:      Multi-Char Listbox Component (TBillListBox)
}
unit TBillist;

interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, FindDlg, Buttons, DsgnIntf, About;
type
  TAboutProperty = class(TPropertyEditor)
public
  procedure Edit; override;
  function GetAttributes: TPropertyAttributes; override;
  function GetValue: string; override;
end;
type TItemsProperty = class(TPropertyEditor)
public
  procedure Edit; override;
  function GetAttributes: TPropertyAttributes; override;
  function GetValue: string; override;
end;
type
  TBillListBox = class(TListBox)
  private
    dlgFind: TdlgFind;
    FAutoSearch: Boolean;
    FAbout: ShortInt;
  protected
    procedure KeyPress(var Key: Char); override;
  public
    procedure FindItemDlg(const SearchItem: string);
    function SearchList(Item: PChar): Integer;
    procedure SetCurrentIndex( CurrIndex: LongInt);
  published
    property AutoSearch: Boolean read FAutoSearch write FAutoSearch
             default False;
    property About: ShortInt read FAbout write FAbout;
end;
var
  CurrentIndex: LongInt;
procedure Register;

implementation

uses Listdlg;
{TAboutProperty Methods.}
procedure TAboutProperty.Edit;
var
  dlgAbout: TAboutbox;
begin
  dlgAbout := TAboutbox.Create(Application);   dlgAbout.ShowModal;
end;
function TAboutProperty.GetAttributes: TPropertyAttributes;
begin
  Result := [paDialog, paReadOnly];
end;

function TAboutProperty.GetValue: string;
begin
  Result := 'About Listbox Gadget...';
end;
{TItemsProperty Methods.}
procedure TItemsProperty.Edit;
var
  dlgEditor: TListEditor;
  objListBox: TBillListBox;
begin
try
  objListBox := (self.GetComponent(0) as TBillListBox);
  dlgEditor := TListEditor.Create(Application);
  dlgEditor.SetReferenceTo(objListBox);
  dlgEditor.lstItems.Clear;
  dlgEditor.lstItems.Items.AddStrings(objListBox.Items);
  dlgEditor.ShowModal;
finally
  dlgEditor.Free;
end;
end;
function TItemsProperty.GetAttributes: TPropertyAttributes;
begin
  Result := [paDialog, paReadOnly];
end;
function TItemsProperty.GetValue: string;
begin
  Result := 'Click to add/modify items...';
end;
{TBillListBox Methods.}
procedure TBillListBox.FindItemDlg(const SearchItem: string);
begin
try
  dlgFind := TdlgFind.Create(self);
  dlgFind.Top := self.Parent.Top + self.Top;
  dlgFind.Left := self.Parent.Left + self.Left;
  dlgFind.edtFind.Text := SearchItem;
  dlgFind.edtFind.SelStart := 1;
  dlgFind.edtFind.AutoSelect:= False;
  dlgFind.ShowModal;
finally
  dlgFind.Free;
end;
end;
function TBillListBox.SearchList(Item: PChar): Integer;
var
  ItemFound: LongInt;
begin   ItemFound := SendMessage(Handle,
                         LB_FINDSTRING, CurrentIndex, LongInt(Item));
  if ItemFound = LB_ERR then
      begin
      CurrentIndex := 0;
      MessageDlgPos('Item not Found.', mtWarning, [mbOK],
                    0, dlgFind.Left + 75, dlgFind.Top);
      end
  else
      ItemIndex := ItemFound;

  CurrentIndex := ItemFound;
  Result := ItemFound;
end;
procedure TBillListBox.SetCurrentIndex(CurrIndex: LongInt);
begin
CurrentIndex := CurrIndex;
end;
procedure TBillListBox.KeyPress(var Key: Char);
begin
{ Activate AutoSearch Dialog if the the AutoSearch property is set to True and
  the Esc key and the Enter key have not been pressed. }
if (AutoSearch) then
   begin
   if ( (Key <> Chr(27)) and (Key <> Chr(13)) ) then
     begin
     { Activate Listbox AutoSearch Dialog }
     self.FindItemDlg(Key);
     { Disable Listbox KeyPress event by setting Key := null }
     Key := #0;
     end;
   end
else
   { Do default bahavior}
   inherited KeyPress(Key);
end;
procedure Register;
begin
  RegisterComponents('Samples', [TBillListBox]);
  RegisterPropertyEditor(TypeInfo(ShortInt), TBillListBox, 'About', 
                                                              TAboutProperty);
  RegisterPropertyEditor(TypeInfo(TStrings), TBillListBox, 'Items',
                                                              TItemsProperty);
end;
initialization
  CurrentIndex := 0;
end.

Listing Two

{ * Unit Name: finddlg.pas
  * Author:    William Stamatakis
  * Note:      Search List Dialog for Multi-Char Listbox Component (TdlgFind)
}
unit Finddlg;
interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, Buttons;
type
  TdlgFind = class(TForm)
    edtFind: TEdit;
    Label1: TLabel;
    btnFindNext: TBitBtn;
    btnClose: TBitBtn;
    procedure ButtonsClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
 end;
var
  dlgFind: TdlgFind;

implementation
uses Tbillist;

{$R *.DFM}

procedure TdlgFind.ButtonsClick(Sender: TObject);
var
  strFind: string;
  pFind: PChar;
begin
if Sender = btnClose then
  begin
  (Owner as TBillListBox).SetCurrentIndex(0);
  Close;
  end
else
  begin
  strFind := edtFind.Text;
  pFind := @strFind;
  StrPCopy(pFind, strFind);
  (Owner as TBillListBox).SearchList(pFind);
  end;
end;
end.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.