November 1997

Building components, part 3

by Sam Azer

In the September 1997 issue of C++Builder Developer's Journal, part 1 of this series looked at the basic elements of component building in C++ Builder. In part 2, we built a small example component. This month, we'll demonstrate how to add some polish to your component--finishing touches like writing a help file to document the component, creating custom property and component editors to make it easier to use, and adding an icon to set the component apart from others. This article is, in essence, three articles in one; so feel free to jump to the section(s) of interest to you.

Let's start with the most important task to perform once the component is working: writing a help file.

A custom help file

As I mentioned last month, the only way to avoid sitting in front of a debugger day and night is to have your project organized in your head before you start working. Doing so requires documentation. Once your component is ready for deployment, you can polish up the documentation to produce online, context- sensitive pop-up help. Making help files is no great joy, but the Microsoft Help Workshop compiler (HCW) that comes with C++Builder greatly simplifies the process. And, the C++Builder IDE makes integration of the help file virtually automatic. We'll take a quick look at the essential steps here.

Basically, you'll use your favorite word processor to prepare the text of your help file. But instead of chapters containing pages, you structure your help file as books containing topics. At the very least, your help file must contain a general overview and a topic for each new property and event your component publishes. Each topic can be as long or as short as you like--there are no physical page constraints. You can easily embed text, images, and simple tables. Don't expect to be able to do too much fancy formatting--the help compiler will choke on some of the more sophisticated features of the major word processors. Stick to simple tables, avoid all but the standard fonts, and try to use 16- or 256-color Windows bitmap (BMP) files. Another trap to avoid is the "keep (paragraph) with next" feature that's used for most headings in a normal document. HCW uses it to mark non-scrolling regions that are allowed only once at the top of a topic--so, don't use your favorite style sheet for your help file topics! Mark the end of each topic using a hard page break ([Ctrl][Enter] in Microsoft Word). HCW requires specific information about each topic in order to produce the final help file. You must title the main topics, provide key words and phrases for the index, and assign Topic IDs to allow for hypertext jumps. If you want the reader to be able to browse through your topics, you must also assign a browse sequence to tell HCW the order of the topics. You pass all this information to HCW by embedding it in footnotes. When you find a footnote marker in a book--like this*--you know that at the bottom of the page will appear a note with an asterisk in front of it. HCW reads your topics in much the same way. If it finds a footnote that uses a dollar sign--like this$--it takes the corresponding footnote text to be the title of the current topic. Let's look at the other footnotes you'll need for each topic, along with the characters that identify them.

K--keyword

Keywords are listed in the index. You can have as many keywords or phrases as you like for each topic, separated by semicolons, as follows:
K TDBShortList; ComboBox; Short List
When a user selects a keyword from the index, the titles of all the topics containing that keyword are listed. The user can then view a topic by clicking on a title.

+--browse code

If you enable browse buttons in your help file, the user can press the Next or Previous button at the top of the help window to move from page to page. The sequence of the pages is determined by the browse code:
+ 001
This code is alphanumeric and is sorted as such--a very handy feature. You can start with browse sequence codes of 001, 002, 003, and so on; then, if you need to add topics later, you can give them codes of 001-01 and 001-02, rather than renumbering all the pages.
#--topic IDs
Each topic must have a unique number, like the following:
# 1001
You can use topic IDs in hypertext jumps. Mark your hypertext using the double-underline attribute, then add the topic ID you want to jump to (don't allow any space between the hypertext and the topic ID), as follows:
..the ListDefaults1001 property..
This example shows the topic ID in color for emphasis, but you should mark the topic ID with the Hidden attribute to indicate to the help compiler that it's a target and not regular text. After you've compiled your help file, the hypertext will appear in green with a single underline:
..the ListDefaults property..
When you click on it, the help engine will jump to the specified topic.

A--A-Keyword

If you select a property or event from the Object Inspector and press [F1], the IDE will search all the A-Keywords and display the matching topic. If more than one match is found, the user will be able to choose from a list.

For a component, the A-footnote must be formatted as the name of the component followed by _Object, a semicolon, and the name of the component again, as follows:

A TDBShortList_Object;TDBShortList
Properties and events are formatted in much the same way. Here are the A-footnotes for the ListDefaults property and the OnAddNew event of
TDBShortList:

A TDBShortList_ListDefaults; ListDefaults_Property;
 ListDefaults

A TDBShortList_OnAddNew; OnAddNew_Event; OnAddNew

Compiling your help file

Once you've prepared your topic file with the proper footnotes, you must export it to Rich Text Format (RTF). If you get frustrated with the RTF-related idiosyncrasies of your word processor, you're not alone. The HCW documentation explains the RTF codes--they're not much more complicated than HTML.

The next step is to make your project file. Start by running HCW.EXE, which resides in the CBuilder\Help\Tools directory. (It helps to put a shortcut to this program into your Start Menu.)

Microsoft's Help Workshop is very easy to use. If you've used previous versions of Microsoft's HC, you'll be impressed with the GUI interface and the new features. In fact, it's so simple to use that you don't really need any instructions here. However, it's worth mentioning that HCW has a nasty bug that causes it to crash if you don't define a main window for your project. So, start by clicking on the Windows... button and adding a window called main. Make a Contents file and a Project file, then compile everything to produce your help file.

After compiling your project, you'll have a working TDBShortList.HLP file. Integrating it with the C++Builder IDE is easy. The documented procedure is to use the OpenHelp tool, but that didn't work on my system--my OpenHelp.INI file was corrupt. An alternate technique that's guaranteed to work is to copy your help and contents files (TDBShortList.HLP/CNT) to the CBuilder\Help directory; then, add the following two lines to your BcbHelp.CFG file (in the same directory):

:INDEX TDBShortList=TDBShortList.hlp
:LINK TDBShortList.hlp
You must delete the Bcb.GID and FTS files to force WinHelp to make new ones. The next time you try to open Bcb.HLP, WinHelp will read BcbHelp.CFG. It will then create a Bcb.GID file that includes references to the topics in TDBShortList.HLP.

To test the keywords in your help topics, click on Bcb.HLP from the Explorer and type TDBShortList in the Index. You should find it and be able to jump to that topic directly. To test your A-Keywords, click on TDBShortList in the C++Builder component palette, then press [F1]. Doing so should give you the same results. Repeat this procedure for each property and event.

Property and component editors

Once you've finished your documentation, your component is complete. However, you may find that it publishes some properties that aren't easy to set. Or, you may want to write a wizard to walk a programmer through the configuration. The C++Builder IDE can be extended to handle either case.

At the moment you drop your component on a form, the form editor scans an internal list of registered component editors for one that can be used with your component. The form editor instantiates (creates and initializes an instance of) this editor, which then repeats the same process--it scans a list of registered property editors and instantiates one for each property your component publishes. The TDefaultEditor and TPropertyEditor classes implement the behavior of a component and property editor, respectively. In both cases, you often can implement the customized functionality you want by deriving a new editor class that overrides only four simple methods! Then, you can register your new editor classes with the C++Builder IDE. To demonstrate how this process works, we're going to build two property editors. We'll use a TOpenDialog for the ListName property, to allow the programmer to browse for the list file. For the ListDefaults property, we'll make a new dialog box to act as a simple list editor. After that, we'll build a basic component editor. Let's get started.

Property editors

First, an important note: Due to limitations in the 1.0 release of C++Builder, the compiler will register editors only for VCL-derived properties. To see this problem, try compiling a line like this one:
void *p = __typeinfo(AnsiString);
The compiler insists that you use a VCL-derived class. In practice, this problem forces all properties requiring a new editor to use a wrapper. Also, because a VCL-derived class must be instantiated using the new operator, the property itself must be a pointer to a wrapper class. As we mentioned, you must override four basic methods to implement most basic property editors. They are as follows:
TPropertyAttributes __fastcallGetAttributes(void);
AnsiString __fastcall GetValue(void);
void __fastcall SetValue(AnsiString v);
void __fastcall Edit(void);
GetAttributes() tells the Object Inspector about the characteristics of your property editor. There are a number of attributes you can use--to see a list, run a keyword search from the Help menu for GetAttributes.

The Object Inspector calls Edit() to activate your editor. GetValue() and SetValue() are simple functions to convert your property to and from text form for display in the Object Inspector. The only way your editor can actually get access to the value is through one of the methods provided by TPropertyEditor. Again, due to temporary restrictions in the 1.0 release of C++Builder, the primary method to use is GetOrdValue(). It was designed to return an integer value, but it can return a pointer as well. The pointer can then be cast into the desired type and dereferenced. Listing A shows the code for the ListName property and editor. It's a little kludgy, but it still works. I hope there will be a void * GetPtrValue() method in the 2.0 release--and no restrictions on the __typeinfo() function. Designer is also a member of the TPropertyEditor class. You must call the Modified() method of any property editor's Designer member after making changes to the value of the property. This is the only way to alert the Object Inspector to the fact that changes have taken place.

The RegisterPropertyEditor function, as you can see in Listing A, takes four parameters. The first is a pointer to the type information for the property to be edited. The last is a pointer to the editor class for the specified property. The remaining two are optional. If you specify a class and property name, the property editor will be used only for that class and property. Otherwise, it's used for all instances of the property type.

After you've added the required code to TDBShortList, an ellipsis will appear at the end of the ListName property. Clicking the ellipsis will open a TOpenDialog box. If the modal result of that box is true, the new ListName is copied to the property.

The ListDefaults property is a little more complicated. Choose File | New Form... from the main menu to create a blank form. Populate it with an edit field, a list box, and Add, Delete, and Close buttons. Then, write the code that adds the text from the edit field to the list box and that deletes the currently selected list-box item. Figure A shows my version of this form.

Figure A:We designed this CommaText List Editor form.
Figure A

The code for most of the CommaText list editor is found in Listing B on the following page. You'll need to write the handlers for the Add and Delete buttons, but the code is typical for a dialog box. The twist is that it's easier to register the control if you copy the component header and code into the respective form files and use them as your new component files. You'll eventually want to use more dialog boxes and resources, at which point you'll be forced to use additional files. When that happens, use the link and resource pragmas to tell C++Builder about the files.

The only thing that's new in Listing B is the GetEditLimit() method of TPropertyEditor. By default, this method returns a maximum property length of 256 characters; you must override it if you need more.

A component editor

Component editors let you perform operations on a component as a whole. Again, there are four basic methods to override in most cases. This time, though, a pointer to the component is passed as a parameter to the component editor's constructor--which helps make the code more clear.

As we mentioned earlier, the form editor instantiates a component editor at the moment you drop your component on the form. At that time, the form editor also asks the component editor how many tools it provides. Your component editor can actually offer many support tools to the programmer; they're listed at the top of the speed menu that appears when you right-click your component from the form editor. Each of these tools is called a verb in VCL-speak. Figure B on the next page shows the speed menu produced by the component editor code from Listing C on page 7.

Figure B:Our component editor generates this speed menu for TDBShortList.
Figure B

You must override the GetVerbCount() method of TDefaultEditor to return the number of verbs your editor offers. You must also override the GetVerb(i) method to return the strings used in the speed menu for each verb--the method is called once for each verb, where i is 0 for the first verb, 1 for the second, and so on. Finally, to run your verbs, you must override the ExecuteVerb() method.

If the programmer double-clicks on the component, the form editor calls ExecuteVerb(0)--the first verb. If a component editor is selected from the speed menu, ExecuteVerb(i) is called with i representing the verb to execute in the same order as returned by GetVerb(i). Again, the pointer to your component is passed in the constructor for your component editor. The constructor also gets a pointer to a class called TFormDesigner, which has another method called Modified(). As before, you must call that method if your component editor changes any properties. TDefaultEditor has an Edit() method that loops through the list of properties for a component. For each property, Edit() calls EditProperty(). As you can see from the end of Listing C, the first two verbs are handled by calling the property editors for ListDefaults and ListName. The second parameter to EditProperty() is a flag that tells Edit() to stop looping through the list of properties. The last parameter is a flag that can be used to destroy the property editor--you may want to avoid that one!

A custom icon

Well, at this point you've done just about everything. The component is available to the programmer, it's documented, and it's easy to use. There's just one last thing to do: Make a distinctive icon to set your component apart from the others.

Creating the icon is very easy: Simply make a RES file containing an icon and give it the same name as your component. Let's work through the steps involved.

Start the Image Editor and create a new resource file by choosing File | New | Resource File. Then, select New Icon from the Resource menu. The dialog box that opens doesn't give you many choices--it specifically lacks the 24- by 24-bit Icon option that the Component Writer's Guide says you'll need. No problem; go for the closest thing--32 by 32 bits with 16 colors. It turns out that C++Builder will try to fit whatever you give it, so this option will work. Click OK, and you'll have an icon named Icon1.

Right-click on the icon and select Rename from the speed menu, then assign the icon the same name as your component. Be sure to use all uppercase: TDBSHORTLIST, for example. Next, right-click on the icon and select the Edit option to edit your new icon.

Personally, I'm no artist. Even if I was, 32 by 32 bits isn't much space to work with. I put together an icon based on a Notepad document, as shown in Figure C--it's not fancy, but it will do.

Figure C:We created this icon for TDBShortList.
Figure C

Finally, save the resource file by using the name of the component. Place TDBShortList.RES in the same directory as the other component files, then rebuild the component palette by choosing Component | Rebuild. Your custom icon will now appear in place of the default icon.

Wrap up

If you've been following the three parts of this series, by now you should be feeling pretty confident about writing new components. Our example, TDBShortList, demonstrates the basic concepts involved in creating a new component by extending an existing one. In most cases, the information and techniques presented in this series will be enough to get the job done. There's more, though--be sure to look for additional details in future issues of C++Builder Developer's Journal! Listing A: The ListName property and editor
// VCL wrapper class for an AnsiString
class TText : public TPersistent 
{ public:
  __fastcall TText() : TPersistent() {};
  __fastcall ~TText() {};
  PROP_PUBLISHED(AnsiString,Text,nodefault);
}
.
.
// from class TDBShortList, change ListName to:
PROP_PUBLISHED(TText*,ListName,nodefault);
.
.
class TTextProperty : public TPropertyEditor 
{ public:
  virtual AnsiString __fastcall
    GetValue(void) { return 
    ((TText*)GetOrdValue())-&gtText; };
  virtual void __fastcall
    SetValue(AnsiString v) {
    ((TText*)GetOrdValue())-&gtText = v; };
  virtual TPropertyAttributes __fastcall
    GetAttributes(void) 
    { return (TPropertyAttributes() <<
      paDialog << paRevertable); };
  TTextProperty() : TPersistent() {};     
};
.
.
class TFileNameProperty : public TTextProperty 
{ public:
  virtual void __fastcall Edit(void);
  TFileNameProperty() : TTextProperty() {};     
};
.
.
// in the register() function:
RegisterPropertyEditor(
  __typeinfo(TText),
  __classid(TDBShortList),"ListName",
  __classid(TFileNamesProperty) );
.
.
// in the constructor for TDBShortList:
  ListName = new TText;
//in the destructor for TDBShortList: delete ListName;
.
.
#include "dir.h"
void __fastcall TFileNameProperty::Edit(void)
{
  TOpenDialog *o=new TOpenDialog(Application);

  o-&gtFileName = GetValue();
  o-&gtDefaultExt = "*.lst";
  o-&gtFilter = "List Files (*.lst)|*.lst"
            "|All Files (*.*)|*.*";
  o-&gtFilterIndex = 1;
  o-&gtTitle = "List File Name";

  char buf[MAXPATH+1];
  o-&gtInitialDir = getcwd( buf, sizeof(buf) );

  if ( o-&gtExecute() == true )
  {  SetValue( o-&gtFileName );
    Designer-&gtModified();
  };
  delete o;
}
Listing B: The CommaText property editor
// max chars in ListDefaults string
#define MAXLISTDEFAULTSTEXT 4096
.
.
class TCommaTextProperty : public TTextProperty
{
  public:
    virtual int __fastcall GetEditLimit(void)
    { return MAXLISTDEFAULTSTEXT; };

    virtual void __fastcall Edit(void);
    TCommaTextProperty() : TTextProperty() {};
};


class TCommaTextForm : public TForm
{
__published:   // IDE-managed Components
  TListBox *ListBox;
  TBitBtn *BClose;
  TBitBtn *BAdd;
  TBitBtn *BDelete;
  TEdit *Item;
  void __fastcall BAddClick(TObject *Sender);
  void __fastcall BDeleteClick(TObject *Sender);
private:// User declarations
  AnsiString Get(void) 
    { return ListBox-&gtItems-&gtCommaText; };
  void Set(AnsiString v) 
    { ListBox-&gtItems-&gtCommaText = v; };
public:// User declarations
  __fastcall TCommaTextForm(TComponent* Owner) :
                            TForm(Owner) { };
  __property AnsiString CommaText = { 
                          read=Get, write=Set };
};
.
.
// in the register() function:
RegisterPropertyEditor( __typeinfo(TText),
                        __classid(TDBShortList),
                        "ListDefaults",
                        __classid(TCommaTextProperty)
                      );
.
.
// in the constructor for TDBShortList:
  ListDefaults = new TText;
// in the detructor for TDBShortList: delete ListDefaults;
.
.
void __fastcall TCommaTextProperty::Edit(void)
{
  TCommaTextForm *f= new TCommaTextForm(Application);

  f-&gtCommaText = GetValue();
  f-&gtShowModal();

  if ( GetValue() != f-&gtCommaText )
  { if (f-&gtCommaText.Length() > MAXLISTDEFAULTSTEXT)
    {  MessageBox( 0, ("The limit is " +
       AnsiString(MAXLISTDEFAULTSTEXT) +
       " characters. The text will be truncated.")
       .c_str(), "Sorry - too much text",
       MB_ICONEXCLAMATION );

       f-&gtCommaText.SetLength(MAXLISTDEFAULTSTEXT);
  };
    SetValue( f-&gtCommaText );
    Designer-&gtModified();
  };

   delete f;
}
Listing C: The TDBShortList component editor
class TDBShorListEditor : public TDefaultEditor
{
  private:
    TDBShortList *Component;
    TFormDesigner *Designer;
    int index;

  public:
    virtual void __fastcall EditProperty(
            TPropertyEditor *pe,
            bool &con, bool &fre );

    void Reset(void); // demos use of comp ptr

    virtual int __fastcall GetVerbCount(void)
    { return 3; };

    virtual AnsiString __fastcall GetVerb(int i)
    { const char *verb[] = { "ListDefaults",
                     "ListName", "Reset Example" };
      return verb[i]; };

    virtual void __fastcall ExecuteVerb(int i)
    { if ( i < 2 ) { index = i; Edit(); } 
         else Reset(); };

    void Modified(void) 
    { if ( Designer ) Designer-&gtModified(); };

    __fastcall virtual ~TDBShorListEditor(void) {};
    __fastcall virtual DBShorListEditor(
                 TComponent* c, TFormDesigner* d) :
                 TDefaultEditor(c, d)
    {Component= (TDBShortList *)c; Designer = d;};
};
.
.
RegisterComponentEditor(__classid(TDBShortList),
                        __classid(TDBShorListEditor));
.
.
void TDBShorListEditor::Reset( void )
{
  if ( !Component || !Designer ) // be careful
    return;

  if ( MessageBox( 0, 
    "Reset the component to the example defaults?",
    "Are You Sure?", MB_YESNO ) != IDYES )
  return; // customer said no... so quit

  Component-&gtListDefaults-&gtText = "Red, Yellow, Blue";
  Component-&gtListName-&gtText = "color.txt";
  Modified();
}

void __fastcall TDBShorListEditor::EditProperty(
     TPropertyEditor *pe, bool &con, bool &fre )
{
  if ( !strcmpi( pe-&gtGetName().c_str(), 
       !index ? "ListDefaults" : "ListName" ) )
  {  pe-&gtEdit();
    con = false;
  };
}