Component Building for the Professional

By: Ray Konopka

Abstract: This paper describes the critical extra steps that must be taken in order to develop professional, commercial-quality components.

This paper describes the critical extra steps that must be taken in order to develop professional, commercial-quality components. Specific topics will include creating online component help, effectively designing packages for component distribution, supporting multiple versions of Delphi, and how supporting automatic one-step installation.

Once a component has passed testing, the component building process is often considered complete, and the component distributed to end users. Although this can be done (and all too often is done) the results can be detrimental to the overall success of the component. This is especially true if you are writing components that will be used by other developers, or if you plan on selling your components commercially.

The truth is that the commercial component market simply will not tolerate unprofessional components, and releasing a component without professional features, such as online help, will not be accepted by this ever-growing market. This paper shows you how to add the professional features necessary to make your components suitable for commercial release. Note that this does not mean that these techniques should only be used if you are creating a third-party component library-on the contrary, these techniques are just as beneficial to in-house software departments as they are to commercial customers.

So, what are these professional features? As mentioned above, a professional component must provide online help accessible through the Delphi environment. Because Delphi provides an extensible help system, and providing online help support requires no changes to the component itself, there is no excuse not to provide this feature.

Another feature for consideration is support for internationalization. This does not necessarily mean creating international versions of your component. Instead, it means taking the necessary steps that make this task easier if it ever needs to be performed. Specifically, components that use literal strings should consider using a string table resource.

It has become common practice for third-party developers to include the source code for their components with the shipping library. I fully recommend this, by the way. However, doing so presents a potential problem. This problem may arise if the user decides to recompile your source code-to include debug information, for example. Because you cannot be certain how Delphi will be configured for each user, you should take special precautions to ensure that your components compile correctly-or you'll suffer the headache of many support calls.

And let's not forget about deployment and installation. Although installing new components in Delphi is relatively easy, there are some techniques that can be used to make the installation process run more smoothly.

Online Help

There are two ways in which a user can request context-sensitive help on a component within the Delphi design environment. First, the user can press F1 while the Object Inspector has the focus. In this case, Delphi displays the help associated with the currently highlighted property or event. The second way occurs when the user presses the F1 key when a form has the focus. In this instance, Delphi displays the help screen associated with the currently selected component.

However, context-sensitive help is only available for the components, properties, and events that Delphi knows about. For example, if we drop a new custom component onto a form, select one of its properties in the Object Inspector, and then press F1, Delphi displays the Topics Found dialog box with a list of topics. Unfortunately, none of these topics will be correct because we have not yet instructed Delphi where to find help for the new component. In order to do this, we must first create the component help file.

Creating The Help Document

The first step in creating a component help file is to create a Rich Text Format (RTF) file that contains the help information to be displayed. This RTF file will be referred to as the help document for the component. This section is not intended to describe the entire process of creating a Windows help file. Instead, it focuses on the issues specific to building a component help document.

For a step-by-step guide through the process of creating a general help file, take a look at the Creating Windows Help file, CWH.HLP. It can be found in the Delphi 1 Bin directory or with the source code included with this paper. It is very well organized, and demonstrates many online help features.

There are two basic ways of creating the help document. First, any word processor, like Microsoft Word or WordPerfect, that can handle RTF files can be used. Second, a help-authoring tool like HelpScribble, DotHelp, ForeHelp or RoboHelp can be used to create the document. The example presented in this paper uses the word-processor approach to avoid describing product-specific features.

The help document is organized into a series of pages, with each page representing a help screen. For example, selecting the Help|Contents menu item in Delphi displays the Help Topics screen. Each page specifies several footnotes. These footnotes convey information to the search engine and are not displayed in the resulting help file. Table 1 lists the common footnote types.

Table 1: Footnotes processed by WinHelp.

Symbol Identifies Purpose
# Context String Used to uniquely identify the topic.
$ Topic Title Used to specify title of a topic. This title is displayed in the Topics Found and History windows.
K Visible Keywords Used to specify words or phrases associated with a topic. Each keyword is listed on the Index page in the Help Topics dialog box.
A Invisible Keywords Used to specify words or phrases associated with a topic. A-keywords never appear on the Index page in the Help Topics dialog box.

Jumping to a related topic and displaying a popup window are two basic features used quite extensively in component help files. Each feature relies on a hotspot on which the user clicks to start the action. The two types of hotspots are distinguished by how they are formatted. In particular, to create a hotspot that jumps to another topic, apply double-underline formatting to the text. Immediately after the hotspot text, you must specify the context string of the topic to jump to, but the context string must be formatted as hidden text. The same steps are used to create a hotspot that displays a popup window, but instead of using a double-underline, the hotspot text is formatted with a single underline.

Let's take a look at a real example. Figure 1 shows the first page of the RkLaunch.rtf file, which contains the online help information for the RkLauncher component. The figure has been annotated to indicate the various font styles and sizes used throughout the page. The first page in a help document generally presents an overview of the component and provides links to the component's properties, methods, and events.

Figure 1:  Any word processor that supports RTF files can be used to create a help document.

In Delphi 1 and 2, these links are located beneath the title and appear as hotspots to popup windows. The links in Delphi 3 and later also appear as hotspots below the title, but instead of invoking a popup window, a separate secondary window is displayed.

I used a slightly different approach in designing the help for this book. The main page for each component contains a link to a secondary Class Inspector window, which separates a component's properties, methods, and events into three pages within the window. Once the Class Inspector is displayed, it remains visible allowing the user to quickly jump from one item to another within the component class. Figure 2 shows the Class Inspector in action.

Figure 2: The Class Inspector help window makes it easy to jump between a component's properties, methods, and events.

The link to the Class Inspector is defined as a topic jump. This can be seen in Figure 3, which shows the hidden context strings following the link. The same technique is also used for creating links to items from within the component's description. Notice that FileName in the description body is specified as a topic jump to that property.

Figure 3:  A topic jump consists of a label followed by a context string, which must be formatted as hidden text.

Now let's take a closer look at the footnotes specified in Figure 1. The lines on the left map each footnote symbol in the body to its corresponding footnote string. The #-footnote specifies the context string for this topic to be hlp_TRkLauncher. The $-footnote specifies the title of the topic, and the K-footnote specifies two search strings, "TRkLauncher" and "Launcher." These search strings will appear on the Index page in the Help Topics dialog box. Therefore, the user can find help for this component by searching for "TRkLauncher" or just "Launcher."

A-Footnotes

The A-footnote specified in Figure 1 deserves special attention. A-footnotes were introduced in version 4 of the WinHelp engine, and are similar to K-footnotes except they do not appear on the Index page of the Help Topics dialog box. A-footnotes have a special purpose in Delphi-they are the mechanism Delphi uses to support context-sensitive help within the IDE.

When the F1 key is pressed, Delphi instructs WinHelp to search for an A-footnote that matches one of the patterns listed in Table 2. If more than one match is found, WinHelp displays the Topics Found dialog box listing all of the matches. Otherwise, the topic defining the A-footnote is displayed.

Table 2: A-footnote formats used by Delphi.

Format

Example

Use for

ClassName

TRkLauncher

Main topic page

ClassName_PropName

TRkLauncher_FileName

Component-specific property

ClassName_MethodName

TRkLauncher_Launch

Component-specific method

ClassName_EventName

TRkLauncher_OnFinished

Component-specific event

PropName_Property

Parameters_Property

General property

MethodName_Method

Execute_Method

General method

EventName_Event

OnClick_Event

General event

Following the guidelines in Table 2, Figure 1 illustrates how to define an A-footnote for a component's main topic page. Likewise, Figure 4 shows that the Parameters property topic page defines the A-footnote as "TRkLauncher_Parameters."

Figure 4: Each property, method, and event occupies its own page within the help document.

I recommend using the component-specific A-footnote formats because they have the best chance of being unique across all components and help files. This means your users will be able to access your online help more quickly.

To provide a link to an item that exists in the Delphi help files-a property for an ancestor class, for example-use the AL (ALink) macro. For example, the following command is used to jump to the BorderWidth property of TCustomPanel:

!AL(TCustomPanel_BorderWidth,1)

See the online help for Help Workshop for more information on the AL macro.

Creating The Help File

After the help document is written, the help file (*.hlp) can be created. This is accomplished by creating a help project file that specifies which help documents will be contained in the help file. Since a single help file can consist of any number of help documents, it's a good idea to create a separate help document (*.rtf) file for each component.

Although you can create the help file by hand (after all, it's just a text file), it's much easier to use the Help Workshop. Located in the Delphi HelpTools directory, Help Workshop is a Microsoft utility program for creating WinHelp version 4 help files. Figure 5 shows the DCDC.hpj project file being edited in Help Workshop.

Figure 5: Help Workshop automates the mundane tasks of creating and managing a help project.

Notice how each component help document (*.rtf) is specified in the [FILES] section. Documents are added to this section by clicking on the Files button. For a description of other features, refer to Help Workshop's online help. After all help documents are added, click on the Save And Compile button to generate the help file. Compiling the project in Figure 5 generates the DCDC.hlp file.

Creating The Contents File

In addition to creating help files, Help Workshop can create contents files (*.cnt). When WinHelp opens a help file, it checks to see if a contents file of the same name exists. If so, WinHelp creates a Contents page in the Help Topics dialog box. The Contents page is like a table of contents for your help file, but instead of specifying page numbers, the contents list provides topic jumps into the help file.

Like a help project file, a contents file is a simple text file. But rather than create the file manually, use the Help Workshop as illustrated in Figure 6. When the DCDC.hlp file is opened in WinHelp, the Help Topics dialog box contains a Contents page as shown in Figure 7

Figure 6: Use Help Workshop to build hierarchical contents files.

Figure 7: WinHelp adds a Contents page to the Help Topics dialog box when it finds a contents file matching the name of the help file being opened.

We now have a compiled help file and a contents file. However, before this new information can be displayed from within Delphi, we must tell Delphi where to find it.

Installing Component Help

Merging our new help file with Delphi's involves two steps. Unlike the two steps required in Delphi 1 and 2 (generating keywords and running HelpInst), the steps required in Delphi 3 and later are much easier and, more importantly, can be automated.

The first step is to add two entries under the HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsHelp registry key. One for the help file, the other for the contents file. Each entry specifies the name of the file and its location on the system. Table 3 shows the values used for the DCDC.hlp and DCDC.cnt files.

Table 3: Registry entries for component help files.

File

String Value

DCDC.hlp

C:DCDCHelp

DCDC.cnt

C:DCDCHelp

The second step is to merge the new component help file with Delphi's. This step is necessary in order for Delphi to provide context-sensitive help for your components. Merging is accomplished by adding two lines to the Delphi3.cnt file. For example,

:Include dcdc.cnt
:Link dcdc.hlp

or

:Include dcdc.cnt
:Index dcdc.hlp

When adding lines to a contents file, make sure the last line ends with a CR-LF pair. Otherwise, the last command will not be executed.

The Include command adds the specified contents file to Delphi's contents list. The effect of this statement is shown in Figure 8. The Link and Index commands instruct the WinHelp engine to search for keywords in the specified file in addition to the Delphi3.hlp file. The difference between the two is that the Index command adds the specified file's keyword index to Delphi's index.

The Link command is used to merge component help files with the Delphi help system to avoid exceeding the maximum index size limit.

Figure 8: Our new component help file is seamlessly integrated into the Delphi help system.

After completing these two steps, context-sensitive help can be requested for new custom components as illustrated in Figures 9 and 10.

Figure 9: Pressing F1 from within the IDE displays help for the selected RkLauncher component.

Figure 10: Use the Class Inspector to jump to a particular property, method, or event.

String Tables

String tables are one of the least-used resource types, which is a shame because they are so easy to use in Delphi, and even easier to use in Delphi 3 and later. String tables store strings that are used in an application. Because the strings are part of the resource file, they can be changed without having to recompile the source code. In addition, moving literal strings from code to a string table frees memory in the data segment.

I highly recommend storing error messages, prompts, and other text in string tables. Besides the advantages given in the previous section, if you're considering distributing your components internationally string tables must handle language differences.

The Easy Way

Delphi 3 and later versions encourage the use of string tables by introducing a new feature that makes it extremely easy to create string resources. Traditionally, string tables had to be created using a tool other than Delphi. Delphi 3 introduces the new resourcestring reserved word directly inside a unit to specify string table entries.

As an example, consider the constructor for the TRkAddress component, which is shown in Listing 1. In particular, notice the literal strings being passed to the CreateLabel method. These strings are good candidates to be converted to resource strings.

Listing 1: The TRkAddress constructor using literal strings.

constructor TRkAddress.Create( AOwner : TComponent );
begin
  inherited Create( AOwner );
  . . .
  FLblFirstName := CreateLabel( 'First Name' );
  FEdtFirstName := CreateEdit;
  FLblLastName := CreateLabel( 'Last Name' );
  FEdtLastName := CreateEdit;
  FLblStreet := CreateLabel( 'Street' );
  FEdtStreet := CreateEdit;
  FLblCity := CreateLabel( 'City' );
  FEdtCity := CreateEdit;
  FLblState := CreateLabel( 'State' );
  FCbxState := CreateCombo;
  FLblZIP := CreateLabel( 'ZIP' );
  FEdtZIP := CreateEdit;
  . . .
end;

Listing 2 highlights the changes needed to convert from literal strings to resource strings. As you can see, the resourcestring keyword is used in a similar fashion to the const keyword. In fact, resource strings are used just like string constants as demonstrated in the new calls to CreateLabel. Rather than pass a literal string, the new resource string identifier is used.

Listing 2: The TRkAddress constructor using resource strings.

implementation
uses
Windows;
resourcestring
  SAddrFirstName = 'First Name';
  SAddrLastName = 'Last Name';
  SAddrStreet = 'Street';
  SAddrCity = 'City';
  SAddrState = 'State';
  SAddRkIP = 'ZIP';
constructor TRkAddress.Create( AOwner : TComponent );
begin
  inherited Create( AOwner );
  . . .
  FLblFirstName := CreateLabel( SAddrFirstName );
  FEdtFirstName := CreateEdit;
  FLblLastName := CreateLabel( SAddrLastName );
  FEdtLastName := CreateEdit;
  FLblStreet := CreateLabel( SAddrStreet );
  FEdtStreet := CreateEdit;
  FLblCity := CreateLabel( SAddrCity );
  FEdtCity := CreateEdit;
  FLblState := CreateLabel( SAddrState );
  FCbxState := CreateCombo;
  FLblZIP := CreateLabel( SAddRkIP );
  FEdtZIP := CreateEdit;
  . . .
end;

By Convention: Constants representing string resource identifiers start with the letter S.

When the new version of the RkAddr unit is used by a package or an application, Delphi automatically generates string table entries corresponding to the strings specified in the resourcestring block. As a result, wherever a resource string identifier is referenced, Delphi generates the code to load the string value from the string table.

The Traditional Approach

As simple as the resourcestring approach is, it may be more appropriate to use the old-fashioned approach of creating the resource file manually. The main reason to do this is to gain access to a numeric identifier that can be used to access the desired string. Consider the following CreateLaunchError function shown in Listing 3. Notice that the LoadStr function is used to return the desired string value. However, in this example, the string is indexed using the numeric error code.

Listing 3: A new and improved CreateLaunchError method.

uses
  ShellApi, LnchMsgs;
function CreateLaunchError( ErrCode : Integer ) : ELaunchError;
begin
  Result := ELaunchError.Create( LoadStr( SLaunchOutOfMemory + ErrCode ));
  Result.ErrorCode := ErrCode;
end;

The resourcestring approach described earlier cannot be used in this way because each string constant is just that-a string, and therefore cannot be used as an offset. Plus, the error codes are not contiguous. When resourcestring is used, Delphi automatically assigns numeric values for each string in the table. Typically, these values are contiguous, but because Delphi handles ID conflicts, there is no way to predict the numeric value. In order for the approach demonstrated in Listing 3 to work, we must be able to control which id matches a particular string. Therefore, we must create a string table by hand.

Creating A String Table

A string table can be created in one of two ways. First, a Windows resource editor, such as Resource Workshop, can be used to create the table. Secondly, you can create a string table manually, whichbecause of the simple structure of the string table resource type, is not an unreasonable alternative.

The format of a string table is quite simple. Each entry in the table consists of a numeric identifier and an associated string. The identifier is used to reference the particular string from within code. Because the integer identifiers have less meaning than their string counterparts, it is common to create a set of constants to represent each string resource identifier.

Listing 4 shows the source code for the LnchMsgs unit, which only consists of an interface section that declares constants for each of the strings to be placed in the string table. The starting value of 42000 is an arbitrary value chosen to avoid conflicting with other string resource identifiers.

Listing 4: LnchMsgs.pas-This unit defines string resource identifiers.

unit LnchMsgs;
interface
{$R LNCHMSGS.RES} // Link in Win32 resource file
const
  SLaunchOutOfMemory = 42000;
  SLaunchFileNotFound = 42002;
  SLaunchPathNotFound = 42003;
  SLaunchSharingViolation = 42005;
  SLaunchSeparateDataSeg = 42006;
  SLaunchInsufficientMemory = 42008;
  SLaunchIncorrectWindowsVer = 42010;
  SLaunchInvalidEXE = 42011;
  SLaunchIncorrectOS = 42012;
  SLaunchForMSDos4 = 42013;
  SLaunchUnknownType = 42014;
  SLaunchLoadRealMode = 42015;
  SLaunchNonReadOnlyDataSeg = 42016;
  SLaunchCompressedEXE = 42019;
  SLaunchInvalidDLL = 42020;
  SLaunchWin32 = 42021;
  SLaunchNoAssociation = 42031;
implementation
end.

Creating the LnchMsgs unit serves three purposes. First, it will be used by the code modules that need to reference these strings; for example, the RkLaunch unit. Second, the LnchMsgs.pas file can be included into the LnchMsgs.rc resource file shown in Listing 5. This allows the resource file to use the same constants, which ensures that the correct string is synchronized to a particular ID. Third, the LnchMsgs unit is responsible for including the LnchMsgs.res file into the current project. LnchMsgs.res is the compiled version of LnchMsgs.rc.

Listing 5: LnchMsgs.rc-String resource file for launch messages.

#include "lnchmsgs.pas"
STRINGTABLE
{
 SLaunchOutOfMemory, "Out of memory or executable file is corrupt"
 SLaunchFileNotFound, "File was not found"
 SLaunchPathNotFound, "Path was not found"
 SLaunchSharingViolation, "Sharing violation or network error"
 SLaunchSeparateDataSeg, "A library required separate data segments for each task"
 SLaunchInsufficientMemory, "Insufficient memory to start application"
 SLaunchIncorrectWindowsVer, "Incorrect version of Windows"
 SLaunchInvalidEXE, "File is not a Windows application"
 SLaunchIncorrectOS, "Application was designed for a different operating system"
 SLaunchForMSDos4, "Application was designed for MS-DOS 4.0"
 SLaunchUnknownType, "Unknown executable file type"
 SLaunchLoadRealMode, "Cannot load a real-mode application"
 SLaunchNonReadOnlyDataSeg, "Cannot load a second instance of an executable file containing multiple, non-read-only data segments"
 SLaunchCompressedEXE, "Cannot load a compressed executable file"
 SLaunchInvalidDLL, "A dynamic-link library (DLL) file is invalid"
 SLaunchWin32, "Application requires Windows 32-bit extensions"
 SLaunchNoAssociation, "No association for specified file type"
}

Once the resource file is created, it needs to be compiled into a binary RES file so that it can be referenced by the LnchMsgs unit and thus be included into the current project using the RkLauncher component. If you are using Resource Workshop, the RES file can be created when the RC file is saved by making sure the RES Multi-save option is checked in the Preferences dialog box.

If you created the RC file manually, you will need to use the command line resource compiler to create the corresponding RES file. For the LnchMsgs.rc file the following command is entered at a DOS prompt:

C:> BRC32 -R LnchMsgs

BRC32 is the Borland Resource Compiler that comes with Delphi 3. It can be found in the Delphi Bin directory. The -R option indicates that the LnchMsgs.rc file should be compiled only. The BRC32 program is also capable of binding a resource file to an existing executable file. For this example, we only need to create the RES file because the binding will be handled by the Delphi compiler.

Once the LnchMsgs.res file is created, it needs to be linked into the component unit using the constants. This is accomplished by using the {$R} compiler directive as demonstrated back in Listing 4.

A Common Include File

I mentioned at the beginning of this paper that it has become common for vendors to include the source code for their components. If you plan on doing this, you should seriously consider using a common include file in all of your component units. Listing 6 shows the include file used by all of the components presented in this book. The purpose of this file is to set compiler directives and conditional defines that govern how the components are compiled.

Listing 6: DCDC.inc-A common include file for components.

{===============================================================================
  DCDC Include File

  This file is included into each component unit and serves as a common
  place to add conditional defines and compiler directives to be used by all
  component units.

  Copyright ) 1995-2000 by Ray Konopka
===============================================================================}

{$IFDEF WIN32}
  {$DEFINE D2_OR_HIGHER}
{$ENDIF}

{$IFNDEF VER80}
  {$IFNDEF VER90}
    {$IFNDEF VER93}
      {$DEFINE D3_OR_HIGHER}
      {$IFNDEF VER100}
        {$DEFINE BCB3_OR_HIGHER}
        {$IFNDEF VER110}
          {$DEFINE D4_OR_HIGHER}
          {$IFNDEF VER120}
            {$DEFINE BCB4_OR_HIGHER}
            {$IFNDEF VER125}
              {$DEFINE D5_OR_HIGHER}
            {$ENDIF}
          {$ENDIF}
        {$ENDIF}
      {$ENDIF}
    {$ENDIF}
  {$ENDIF}
{$ENDIF}


{$IFDEF VER93}
  {$DEFINE BCB_COMPILER}
{$ENDIF}
{$IFDEF VER110}
  {$DEFINE BCB_COMPILER}
  {$OBJEXPORTALL ON}
{$ENDIF}
{$IFDEF VER125}
  {$DEFINE BCB_COMPILER}
  {$OBJEXPORTALL ON}
{$ENDIF}


{== Code Generation Directives ==}

{$F-}    { Force Far Calls }
{$A+}    { Word Align Data }
{$U-}    { Pentium-Save FDIV }
{$K-}    { Smart Callbacks }
{$W-}    { Windows Stack Frame }


{== Runtime Errors ==}

{$IFOPT D+}
  {$R+}    { Range Checking - On - if compiled with Debug Information }
{$ELSE}
  {$R-}    { Range Checking - Off - if compiled without Debug Information }
{$ENDIF}

{$S-}    { Stack Checking }
{$I+}    { I/O Checking }
{$Q-}    { Overflow Checking }


{== Syntax Options ==}

{$V-}    { Strict Var-Strings }
{$B-}    { Complete Boolean Evaluation }
{$X+}    { Extended Syntax }
{$T-}    { Typed @ Operator }
{$P+}    { Open Parameters }
{$IFDEF D4_OR_HIGHER}
{$J+}    { Writeable Typed Constants }
{$ENDIF}

{== Miscellaneous Directives ==}

{$C MOVEABLE DEMANDLOAD DISCARDABLE}    { Code Segment Attribute }
{$G+}    { Delphi 1: 286 Instructions / Delphi 3 & later: Imported Data }
{$N+}    { Numeric Coprocessor }
{$Z-}    { Word Size Enumerated Types }

{$IFDEF WIN32}
{$H+}    { Long String Support }
{$ENDIF}

Using an include file is especially important when distributing the source code along with the compiled versions of your components. If the user decides to recompile your code, you cannot assume that the compiler settings on that machine are the same as the ones you used to develop the component. Therefore, by creating a common include file, no matter what the current compiler settings are, your component units will always be compiled with the correct settings.

For these same reasons, a common include file is essential for multi-person development teams. The include file guarantees that everyone builds the components using the same options.

An interesting aspect of this include file is that it does not specify any settings for the $D, $L, or $Y directives. This is because these directives are associated with including debug information into the compiled unit. Since these options have no impact on how the code is compiled, it is not necessary to specify these. Besides, when a library is built for distribution, debug information is usually turned off. However, the component user may opt to recompile the source code and include debug information. The point is that the common include file does not prevent the user from doing this.

Also notice that this is an include file and not a unit file. Therefore, the Object Pascal include file directive $I is used to effectively embed the contents of the DCDC.inc file into the current source file. As an example, the following code fragment shows the first few lines of the RkPrgres unit:

{=========================================================================
  RkPrgres Unit
  Developing Custom Delphi Components
=========================================================================}
{$I DCDC.INC}
unit RkPrgres;
interface
uses
  Windows, Classes, Graphics, Controls, Menus, ExtCtrls, RkCommon;
type
  TRkProgressBar = class( TGraphicControl )
private

What if you have a component unit that needs to use a different compiler directive? In situations like this, specify the new directive after the include file statement. This way, you can essentially override any of the common settings on a unit-by-unit basis.

Packaging

When you create a new custom component, it is very tempting to add the component to the DclUsr50 package so that it can be quickly installed into Delphi. This is fine for testing, but when it's time to distribute your components to other developers, you'll want to supply custom runtime and design packages for your components. To understand why, we need to take a closer look at the DclUsr50 package.

DclUsr50 is defined as a design-only package, meaning that it can be loaded by the IDE in order to install the components that reside within it. Note that design packages are only used by the IDE-they are never distributed with an application. When an application is "built with packages," the program is compiled so that it utilizes runtime packages. This is a subtle, but important point.

The predefined packages that ship with Delphi come in runtime/design pairs. For example, the runtime package containing the database related components is VclDb50. Its design counterpart is DclDb50. To distribute a database application that uses packages, you also need to distribute the VclDb50.dpl package. If the recipient of your application already has this package, then this step is not necessary. This works because there is only one version of the VclDb50 package.

So what does this have to do with DclUsr50? Actually, quite a bit. DclUsr50 does not have a runtime counterpart, which is not surprising because every developer will have a different set of components within that package. And without a runtime package, all of the components inside DclUsr50 must be statically linked into an application. For one or two components, this won't have much of an impact, but for an entire library of components, installing them into DclUsr50 defeats the purpose of runtime packages.

Yet, this is not the only reason to supply your own packages. As we will see, installing a pre-built package is much easier than adding component units to DclUsr50. In fact, the entire process can be automated.

Creating Custom Packages

There are four steps required to create a package. First, you must decide which type of package to create. There are actually four possible choices, but for component packaging, we can limit the selection to runtime-only versus design-only. Runtime-only packages contain only the units necessary to implement one or more components. A runtime package should never contain property editors, component editors, or other design- specific code. This type of functionality should be placed in a separate design package. I recommend creating the runtime package first because, as we'll see, doing so simplifies the construction of the design-time package.

Next, you must select a name for your package. This may sound like a trivial task, but it is very important to select a name wisely. Recall that packages are designed to be distributed to users. Therefore, package names should be unique even across different versions of the same package.

The third step in creating a package is to determine the units to be contained in the package. For runtime packages, you must specify the units that implement the components to be distributed in the package. For example, the Dcdc20 package contains the RkTabLst unit, which defines the TRkTabbedListBox component class. Secondary units used by the component units must also be added. For example, the IntList unit is added because it is used by the RkTabLst unit. For design packages, you must specify design-related units, which includes units that implement property editors, component editors, and registration units.

Any number of units can be added to a package. The only restriction is that you should not add a unit that is already contained in another package. Doing so prevents Delphi from being able to load both packages at the same time.

The fourth step is to determine which packages are required by the new package. An example will help. Consider a runtime package that contains the RkTrkBar unit. One of the units used by RkTrkBar is Classes. Because Classes is contained in the Vcl50 package, Vcl50 is required by the new package. Design packages often require their runtime counterparts, because the runtime version contains all the component units used by the editors contained in the design package.

The Package Editor

The easiest way to create a new package is to use the Package Editor. Start by selecting File|New and then double-click the Package icon. You will then be asked to enter a file name and description for the new package. By default, the new package will be created in the current directory. To specify a new location, click the Browse button. When entering the package name, it is not necessary to specify a file extension; Delphi automatically adds a DPK extension to indicate a package source file. Clicking on the OK button instructs Delphi to generate the source file and open it up in the Package Editor.

At this point, the package is empty. But before we start adding units, we should specify whether this is a runtime or design package. By default, new packages created in the editor are defined as design-only. To change this, click on the Options button to display the dialog box shown in Figure 11. Make the appropriate selection on the Description page, and then close the Options dialog box to continue editing the package.

Figure 11: To create a runtime package, you must change the usage option from design to runtime.

Next, we need to add the units that will be contained in the package. Simply click on the Add button and select the desired unit. You can also create a new component from the Add dialog, but I don't recommend it because the component resource file is not automatically added to the package at this point in the process.. Figure 12 shows that the Dcdc20.dpk runtime package contains all of the non-data-aware component units presented in this book. Following Borland conventions, I've isolated the data-aware components and placed them into a separate package; specifically, DcdcDb20.dpk.

Figure 12: The main window of the Package Editor isolates the Contains list from the Requires list by displaying it on a separate page.

After populating the Contains list, we need to specify the required packages. Switch to the Requires page and click on the Add button to select a required package. By default, the Vcl50 package is automatically added to the requires list. If you don't add any required packages, when Delphi attempts to compile the package it will inform you if any packages should be added. To save the package source file, select File|Save or right-click in the editor and select Save Package.

Next, you need to compile the package source file by clicking the Compile button. Delphi creates two files when a package is successfully compiled. The first is a package library file (*.dpl), which contains the machine code for the components in the package. The second is a package symbol file (*.dcp), which contains the symbol information for all of the units in the package. Both of these files are necessary to compile an application that uses any of the components contained in the package.

Preventing Unauthorized Access

With the runtime packages created, we can now shift our attention to the design package. However, before we dive into details, we need to examine a problem that may arise with the way the Component Expert generates a component unit. The Component Expert automatically generates a Register procedure at the end of the unit. Under Delphi 1 and 2, having the Register procedure in the component unit poses no threat. In fact, it is quite useful because it allows individual components to be installed.

Unfortunately, under Delphi 3 and later, leaving the Register procedure in your component unit can have undesirable effects. Specifically, your components can be installed by others even without your component unit or package!

This situation arises when someone uses your component in a property editor or component editor, and then implicitly imports your unit into their design package. A unit is implicitly imported into a package when the runtime package containing the unit is not specified on the package's requires list.

When the property editor or component editor using your component is installed, your component will also be installed because Delphi finds the Register procedure defined in the component unit. This results in your component appearing on the component palette even though your component unit and package may not be present.

Fortunately, even though your component can be placed onto a form, the application cannot be compiled because the user does not have the component unit nor the package. But this is still certainly an undesirable effect.

There are two ways to prevent the above scenario from occurring. The first is for property editor and component editor writers to avoid implicitly importing your component units. The second is to remove the Register procedures from your component units and place them in a registration unit. Because we have no control over editor writers, the second option is preferred.

Registration Units

As its name suggests, the purpose of a registration unit is to register components, property editors, and component editors with Delphi. Listing 7 shows the source code for the DcdcReg unit, which is responsible for registering all of the components presented in this book.

Listing 7: DcdcReg.pas-The registration unit for this book's components.

{=========================================================================
  DcdcReg Unit
  Registration Unit for all components presented in "Developing Custom
  Delphi 3 Components" by Ray Konopka.
  In Delphi 3 and later:
  Install the DcdcDcl.dpl design-time package.
  Developing Custom Delphi 3 Components
  Copyright ) 1995-1997 by Ray Konopka
=========================================================================}
unit DcdcReg;
interface
  procedure Register;
implementation
uses
  Forms, Classes, Controls, SysUtils, DsgnIntf, RkCommon,
  RkBtn, RkPanel, RkLabel, RkLblEdt, RkTabLst, RkTabEdt, RkPrgres,
  RkStatus, RkStsEdt, RkTrkBar, RkBWCC, RkAddr, RkNavPnl, RkLaunch,
  RkLcrEdt, RkMail, RkLookup, RkDBStat, RkDBSpin, RkROGrid, RkAbout;

{========================}
{== Register Procedure ==}
{========================}
procedure Register;
begin
  {== Register Components ==}
  RegisterComponents( PalettePage,
                      [ TRkOkButton, TRkCancelButton, TRkPanel, TRkLabel,
                        TRkTabbedListBox, TRkProgressBar,
                        TRkStatusControl, TRkGlyphStatus, TRkClockStatus,
                        TRkKeyStatus, TRkTrackBar, TRkBwccCheckBox,
                        TRkBwccRadioButton, TRkAddress, TRkNavigatorPanel,
                        TRkLauncher, TRkMailMessage, TRkLookupDialog,
                        TRkDBStatusControl, TRkDBSpinEdit,
                        TRkDBReadOnlyGrid ] );

  {== Register Property Editors ==}
  RkStsEdt.Register;
  RkTabEdt.Register;
  RkLcrEdt.Register;
  RkAbout.Register;
  {== Register Component Editors ==}
  RkLblEdt.Register;
end;
end.

Each component unit is added to the uses clause and a single call to RegisterComponents within the Register procedure takes care of registering all of the components. Notice that for each property editor and component editor, the DcdcReg Register procedure simply calls the Register procedure defined in the editor unit. Because property editors and component editors are only used at design-time, there is no harm in keeping the registration code within the unit implementing the editor.

Now that we have a registration unit, we can return our attention to the design package. Instead of looking at the design package in the Package Editor, let's take a look at the DcdcDcl.dpk source code in Listing 8. The package is quite simple thanks to the registration unit and runtime packages created earlier. The contains clause lists all of the design-time specific units including the registration unit. The requires clause specifies the Dcdc20 and DcdcDb20 runtime packages because the DcdcReg unit uses the units found in these two packages.

Listing 8: DcdcDcl.dpk-The design package for this book's components.

package DcdcDcl;
{$DESIGNONLY}
{$DESCRIPTION 'Developing Custom Delphi 3 Components - Design'}
{$R 'DcdcReg.dcr'}
requires
  vcl50, 
  Dcdc50,
  DcdcDb50;
contains
  DcdcReg, 
  RkAbout, 
  RkLblEdt, 
  RkLcrEdt, 
  RkStsEdt, 
  RkTabEdt;
end.

The Component Resource File

Take another look at Listing 8. In particular, look at the compiler directives at the beginning. In addition to specifying the $DESIGNONLY directive and a description, the DcdcReg.dcr component resource file is linked into the package. Component resource files generally only contain the bitmaps to be displayed on the component palette.

The Package Editor searches for a matching DCR file whenever you add a unit to a package. However, the Package Editor does this for both runtime and design packages. But linking palette bitmaps into a runtime package is pointless because the bitmaps will go unused and simply waste space.

Therefore, instead of creating separate DCR files for each component unit, I recommend creating a single DCR file containing all of the component bitmaps. Recall that component resource files are nothing more than Windows RES files with a different extension. Therefore, there is no problem in storing multiple bitmaps as demonstrated in Figure 13, which shows the DcdcReg.dcr file loaded into the Image Editor.

Notice that the name of the resource file is the same as the registration unit. As a result, when the registration unit is added to the design package, the component resource file is also added. Furthermore, since the registration unit is not used in the runtime packages, the component bitmaps will not be linked into them.

Figure 13: The Image Editor supports adding multiple bitmaps to a component resource file.

Don't underestimate the importance of well designed bitmaps to represent your components. The component bitmaps are the first impression users will have of your components. Unprofessional bitmaps give the impression of unprofessional components. If you build components for the commercial market, you may want to get a professional graphics artist to design the bitmaps.

Deployment

The next issue we must face is how to deploy the components to the consumer--that is, other Delphi developers. But before we address the "how," we need to decide on "what" will be deployed. For instance, is it sufficient to distribute only the DPL files for each package?

The answer to this is no, because although deploying the DPL files is sufficient when distributing a compiled application to an end user, the DPL file alone is not sufficient to compile a new application that uses any of the components in the package. In this case, it is necessary to distribute the DCP files as well. Table 4 lists various file types and describes when these files should be deployed to other Delphi developers.

Table 4: Determining which files to distribute.

File Type

Extension

Reasons to Distribute

Runtime Package Library

*.bpl

These files are needed to support the use of the runtime package version of your components. These files are also often used to support design packages.

Runtime Package Symbols

*.dcp

These files are needed to compile a project that uses the corresponding runtime package library.

Design Package Library

*.bpl

These files are needed to install your components and design editors.

Design Package Symbols

*.dcp

Distribute these files if you want to allow others to use your design editors in their own packages.

Compiled Units

*.dcu

These files are needed to compile a project that does not use runtime packages. Do not distribute these files if you want to force developers to use your runtime packages.

Form Files

*.dfm

Same rules as Compiled Units.

Source Files

*.pas

Source code is a convenience to developers. Distributing the source code in place of the compiled units is a valid alternative.

Summary

This paper described the critical extra steps that must be taken in order to develop professional, commercial-quality components. Specific topics included creating online component help, effectively designing packages for component distribution, supporting multiple versions of Delphi, and how to support automatic one-step installation.

Contact Information

Ray Konopka rkonopka@raize.com
Raize Software http://www.raize.com

About the Author

Ray Konopka, Raize Software, Inc.

Ray Konopka is the founder of Raize Software, Inc. and the chief architect for their Raize Components and CodeSite products. Ray specializes in Delphi component development and is a frequent speaker at developer conferences.

Paper originally presented at the 11th Annual Borland Conference, July 2000.



Server Response from: ETNASC03