In our September article "Building Components, Part 1," we looked at how a component interacts with the development environment and the program it’s linked into. We covered enough material that you could easily create your own non-windowed controls without further information. Windowed controls, on the other hand, are a different ball game--creating such controls requires more integration with the Windows environment and a deeper understanding of VCL.
This month, we’ll build an example component. In most cases, you build new components on top of existing ones. So, our example component is an extension to a database combo box that remembers the values you put into it. Since it’s also a windowed control, you’ll be doing business with VCL. Any project--even a small example--should begin with a detailed analysis. Having a clear idea of what you want to do and how you want to do it is the only way to avoid sitting in front of a debugger day after day. Unfortunately, we don’t have space for such an analysis here, so we’ll start with a component and a simple program to test it.
#ifndef TDBShortListH #define TDBShortListH /* TDBShortList.h of June 11th, for Building Components, Part 2. */ #includeListing B: TDBShortList.CPP#include #include #include #include #include #define PROP_PUBLISHED(typ, nam, str ) \ private: typ f##nam; \ __published: __property typ nam = \ { read=f##nam, write=f##nam, str } #define PROP_EPUBLISHED(typ, nam, str ) \ private: typ F##nam; \ __published: __property typ On##nam = \ { read=F##nam, write=F##nam, str } // some helpful but unrelated stuff #ifndef Okay #define Okay 0 int debugf( const char *srcfile, const int srcline, const char *fmt, ... ); #define LOC __FILE__, __LINE__ #endif enum SelectType_e { ST_Error=-1, ST_None, ST_Delete, ST_CR, ST_Click, ST_Exit }; typedef __fastcall void (__closure *TUpdateEvent) ( TObject *Sender, Boolean DblClick, AnsiString Text ); typedef __fastcall void (__closure *TSelectEvent) ( TObject *Sender, SelectType_e EventType, int ItemsIndex, AnsiString Text, bool IndexChanged, bool TextChanged ); class TDBShortList : public TDBComboBox { private: bool isLoaded; // true if list has been loaded __fastcall void CheckResult(void); __fastcall void CallSelect(SelectType_e e); __fastcall void CallAddNew(void); __fastcall void CallDelete(void); protected: AnsiString EnterValue; // value of text // at OnEnter int EnterIndex; // value of index // at OnEnter virtual void __fastcall DblClick(); virtual void __fastcall KeyPress(char &Key); virtual __fastcall void CheckFirstUse(void) { if ( !isLoaded ) { isLoaded++; ListRead(); }; }; virtual void __fastcall Click(void) { TDBComboBox::Click(); CallSelect( ST_Click ); }; virtual void __fastcall DoExit(void) { debugf(LOC,"at DoExit"); TDBComboBox::DoExit(); CheckResult(); CallSelect(ST_Exit); }; virtual void __fastcall DoEnter(void) { debugf(LOC,"at DoEnter"); TDBComboBox::DoEnter(); CheckFirstUse(); EnterValue = Text; EnterIndex = Items->IndexOf(Text); }; public: bool ListDirty; // true==list to write to disk __fastcall TDBShortList(TComponent* Owner); __fastcall ~TDBShortList() { debugf(LOC, "destroying TDBShortList" ); }; virtual void __fastcall ListRead(void); virtual void __fastcall ListWrite(void); virtual __fastcall void ItemAddNew( AnsiString Text); virtual __fastcall int ItemDelete( AnsiString Text); __published: PROP_PUBLISHED(AnsiString, ListName, nodefault); PROP_PUBLISHED(AnsiString, ListDefaults, nodefault); PROP_PUBLISHED(bool, AutoWrite, default=true); PROP_EPUBLISHED(TUpdateEvent, AddNew, nodefault); PROP_EPUBLISHED(TUpdateEvent, Delete, nodefault); PROP_EPUBLISHED(TSelectEvent, Select, nodefault); }; // end of class TDBShortList #endif
/* TDBShortList.cpp of June 11th, for Building Components, Part 2. */ #includeTo begin, choose File | New Application from C++ Builder’s main menu. Save the resulting Unit1.CPP file as Develop.CPP and the project as Dev.MAK. In Develop.H, add to the list of includes the line#include #include #pragma hdrstop #include "TDBShortList.h" static inline TDBShortList *ValidCtrCheck() { return new TDBShortList(NULL); } namespace Tdbshortlist { void __fastcall Register() { TComponentClass classes[1] = {__classid(TDBShortList)}; RegisterComponents("Cobb Group", classes, 0); } } __fastcall TDBShortList::TDBShortList(TComponent* Owner) : TDBComboBox(Owner) { debugf(LOC, "constructing TDBShortList" ); FAddNew = NULL; FDelete = NULL; FSelect = NULL; AutoWrite = true; isLoaded = false; } void __fastcall TDBShortList::ItemAddNew( AnsiString itemText ) { debugf( LOC, "ItemAddNew( \"%s\" )", itemText.c_str() ); Items->Add(itemText); if ( Text != itemText ) Text = itemText; ListDirty = True; if ( AutoWrite ) ListWrite(); } int __fastcall TDBShortList::ItemDelete( AnsiString itemText ) { int i = Items->IndexOf(itemText); debugf( LOC, "ItemDelete(\"%s\") indexof()==%d\n", itemText.c_str(), i ); if ( i == -1 ) return !Okay; // item not found Items->Delete(i); ListDirty = True; if ( AutoWrite ) ListWrite(); return Okay; } void __fastcall TDBShortList::ListWrite(void) { // The ComboBox list is copied to a temp. list TStringList *Work = new TStringList; Work->AddStrings( Items ); // Default items are also copied to a temp. list TStringList *Defs = new TStringList; Defs->CommaText = ListDefaults; // Default items removed from the temp. list for ( int i=0; i < Defs->Count; i++ ) { int r = Work->IndexOf( Defs->Strings[i] ); if ( r != -1 ) Work->Delete(r); }; delete Defs; // Then the list of items is written to disk Work->SaveToFile( ListName ); ListDirty = False; delete Work; debugf( LOC, "%d List Items written to %s", Items->Count, ListName.c_str() ); } void __fastcall TDBShortList::ListRead(void) { // Start by loading default items Items->Clear(); Items->CommaText = ListDefaults; debugf(LOC, "Default List Items Loaded" ); // Check if we have read access to the list if ( access( ListName.c_str(), 4 ) ) return; // and return if we don’t // Then add the items in the file TStringList *FileList = new TStringList; FileList->LoadFromFile( ListName ); Items->AddStrings( FileList ); delete FileList; debugf(LOC, "List loaded from %s, %d items", ListName.c_str(), Items->Count ); }; void __fastcall TDBShortList::KeyPress(char &Key) { // Let the combobox see the key first TDBComboBox::KeyPress(Key); // If it’s a return key, process any new entries if (Key == ‘\r’) { CheckResult(); CallSelect(ST_CR); }; }; // Call user’s select event handler, if one exists void __fastcall TDBShortList::CallSelect( SelectType_e EventType ) { static const char *seltypes[] = { "ST_Error","ST_None","ST_Delete","ST_CR", "ST_Click","ST_Exit" }; debugf( LOC, "@ CallSelect( SelectType_e %s )", seltypes[EventType + 1] ); if ( FSelect ) { int NewIndex = Items->IndexOf(Text); FSelect( this, EventType, NewIndex, Text, NewIndex != EnterIndex, Text != EnterValue ); }; } void __fastcall TDBShortList::CallDelete(void) { if ( FDelete ) FDelete( this, True, Text ); else { AnsiString s = "Would you like to remove \"" + Text + "\" from this list?"; if ( ::MessageBox( Handle, s.c_str(), "Delete Item?", MB_ICONQUESTION + MB_YESNO ) == IDYES ) ItemDelete( Text ); CallSelect(ST_Delete); }; if ( AutoWrite && ListDirty ) ListWrite(); } void __fastcall TDBShortList::CallAddNew(void) { if ( FAddNew ) // If one exists... FAddNew( this, False, Text ); else { AnsiString Text2 = Text, s = "Would you like to add \"" + Text2 + "\" to this list?\r\n"; if ( ::MessageBox( Handle, s.c_str(), "New Item", MB_ICONINFORMATION | MB_YESNO ) == IDYES ) ItemAddNew( Text2 ); }; if ( AutoWrite && ListDirty ) ListWrite(); } void __fastcall TDBShortList::CheckResult(void) { if ( AutoWrite && ListDirty ) ListWrite(); // Quit if nothing or no change... if ( (Text == "") || (EnterValue == Text) ) return; // Check if this is a new item if ( Items->IndexOf( Text ) == -1 ) CallAddNew(); } /* If the operator dblClicks on an existing entry, offer to delete it; if the operator dblClicks on a new entry, offer to add it. */ void __fastcall TDBShortList::DblClick() { // Let the ComboBox see the dblClick TDBComboBox::DblClick(); debugf( LOC, "DblClick(), Text == \"%s\"", Text.c_str() ); if (Text == "") return; // no text, so ignore this event // Now check for new item if ( Items->IndexOf(Text) == -1 ) { CallAddNew(); return; } else CallDelete(); }; /* Debugf() - a helpful function but don’t overflow the internal buffer... */ int debugf( const char *f, const int l, const char *fmt, ... ) { char buffer[1024]; sprintf( buffer, "%s @ %d: ", f, l ); va_list argptr; va_start(argptr, fmt); int n = vsprintf( buffer + strlen(buffer), fmt, argptr ); va_end(argptr); OutputDebugString(buffer); return n; }
#include "TDBShortList.h"In the class definition for TForm1, add the following lines of code:
private: // User declarations
TDBShortList *Example;
public: // User declarations
__fastcall ~TForm1()
{ debugf(LOC,"destroying form1");};
Now, use the form editor to add a DataSource component, a DBNavigator component, and a TTable component to TForm1. Set the TableName property for the table to Color.DB; set the other properties as you’d expect: The DataSet for the DataSource is Table1 and the DataSource for the Navigator is DataSource1. All other properties keep their default values. At this point, your form should look like the one shown in Figure A.
You’ll need a small data file to play with. Since you require only a single small text field, it’s no problem to have the program
create the file as needed. For this example, add the code from Listing C to Form1’s OnFormActivate event.
Figure A: Create this form in the Dev project.
Listing C: Form1’s OnFormActivate event
// Check if the file exists
if ( access( Table1->TableName.c_str(), 0 ) )
{ // No, so create it.
debugf( LOC, "%s not found, creating...",
Table1->TableName.c_str() ); // Should be
//color.db
Table1->Active = false;
Table1->TableType = ttParadox;
Table1->FieldDefs->Clear();
Table1->FieldDefs->Add("Color", ftString, 20,
false);
Table1->IndexDefs->Clear();
Table1->CreateTable();
Table1->Active = true;
} else
{ debugf( LOC, "%s exists!",
Table1->TableName.c_str() );
Table1->Active = true;
};
As we discussed last month, the form editor normally writes property values to a form file that’s later bound to the executable as a
resource. When you execute the form, code in TForm loads the resource, then creates and initializes an instance of each component. Since you’re trying to test this component without the form editor for now, you must take a different approach to get the component onto the form: Do it programmatically. In Develop.CPP’s constructor for TForm1, add the code shown in Listing D.Listing D: Develop.CPP’s Tform1 constructor
debugf(LOC, "adding the Example component to TForm1" ); Example = new TDBShortList(this); Example->Parent = this; Example->Name = "Example1"; Example->Left = 75; Example->Top = 25; Example->Width = 150; Example->Height = 24; Example->DataField = "Color"; Example->DataSource = DataSource1; Example->ListName = "colors.txt"; Example->ListDefaults = "Red,Yellow,Blue"; Example->AutoWrite = "True"; Example->Enabled = true; Example->Visible = true; Example->TabOrder = 1; Example->TabStop = true; Example->DropDownCount = 8; Example->ItemHeight = 16; Example->Style = csDropDown;This code begins by creating an instance of a TDBShortList using the new operator, but the most important step is the next one:
Example->Parent = this.This assignment looks like it points the component’s Parent property to the form--which would be useless, since the form is supposed to control the component. Fortunately, the assignment is deceiving.
It turns out that Parent is a property of TControl--it’s not a variable. Parent’s setter function is SetParent, which executes the equivalent of a
this->InsertControl(Example)method call. In other words, our innocent-looking assignment statement doesn’t tell the component anything about the form--instead, it tells the form about the component! Specifically, it adds a pointer to TDBShortList to TForm1’s list of components, thereby making the desired connection between the two. The next time TForm1 is told to show itself, it will display your TDBShortList. The other assignments in Listing D set the remaining property values using hard-coded constants.
At this point, you’re almost ready to run your demo program. In the C++Builder Project Manager, click the Add File To Project button and add TDBShortList.CPP--a.k.a., your component--to the project, as shown in Figure B. Then, save the project. As usual, you can press [F9] to run Dev.EXE.
When the program begins running, there’s a short delay as it searches for the Color.DB file. Since it can’t find the file, the program creates a new one. When the program shows the form, the component appears, as illustrated in Figure C.
Figure B: Add your component to the project.
Figure C: Running Dev.EXE displays your component.
This is a good time to play with Dev.EXE. Type different color names into the various records of the data file, then look carefully at the debug output that appears in the IDE when you press [Alt][F4]. You can add event handlers for the OnSelect, OnAddNew, and OnDelete event properties. Just add your event-handling method to the TForm1 class, then point the property to it in the constructor as with the other properties. I suggest you also try putting an extra input field on the test form--otherwise, the DoExit() event isn’t called when you click on a navigator button. Finally, you can add more debug statements throughout the code to see what happens.
TDBShortList’s main job is to monitor the values entered into the TDBComboBox text field. So, you need to know when the combo box gets and loses focus. When it gets focus, you want to check the color name; when it loses focus, you want to check again to see if the color name has changed. If the new color isn’t in your dropdown list, you should prompt the operator to find out what to do.
TDBComboBox offers OnEnter and OnExit events for this purpose, but you don’t want to use those--they should be available to the programmer who’ll be using your component. However, a solution is close by: The VCL documentation refers to two virtual methods-- DoExit()and DoEnter()--which reside in TWinControl, an ancestor of TDBComboBox. You can override these methods by defining your own DoExit() and DoEnter() methods. They appear as follows in Listing A:
virtual void __fastcall DoExit(void)
{
debugf(LOC,"at DoExit");
TDBComboBox::DoExit();
CheckResult();
CallSelect(ST_Exit);
};
virtual void __fastcall DoEnter(void)
{
debugf(LOC,"at DoEnter");
TDBComboBox::DoEnter();
CheckFirstUse();
EnterValue = Text;
EnterIndex = Items->IndexOf(Text);
};
Now, when your component gets and loses focus, TWinControl tries to call its DoExit() and DoEnter() methods--but TDBShortList’s versions get called instead.
In this case, you’re trying to add your own tasks to the process. You’re not replacing TWinControl’s DoExit() and DoEnter() and functionality. In fact, you may not know exactly what those functions do or if they’ve already been overridden by other classes in the hierarchy. So, use the scope resolution operator to call TDBComboBox::DoExit() and TDBComboBox::DoEnter().
Calling a virtual method’s ancestors ensures that whatever tasks those methods are supposed to do get done. In this case, the documentation says that the ancestor is TWinControl, but there’s no need to tell the compiler--you inherited TDBComboBox, so use its methods and let runtime binding determine which class actually gets the call.
Now getting back to our component, TDBShortList lets the operator add or delete items by double-clicking. TDBShortList also performs better if you check for clicks and carriage returns. TWinControl offers a virtual method called KeyPress(), and TControl offers virtual methods called Click() and DblClick(). Again, override these methods to add your processing to the associated events, and remember to call the ancestors to ensure they do their jobs.
The CheckResult() method in Listing B (TDBShortList.CPP) is called by DoExit(). Its main purpose is to check the TDBComboBox text to see whether the color name is in the Items list. If not, CheckResult() calls the CallAddNew() function. Effectively, AddNew is an event. In this case, its trigger is simply new text that has been detected in the combo box. The CallAddNew() function first checks FAddNew, the closure that’s published as the OnAddNew property. If the closure is null, the function uses a default event handler. Otherwise, it calls the method that the closure points to.
Figure D: After installation, you'll have a new tab and icon on your component palette.
Click on image to view full size.
Figure E: We created this Test application to use our component.
Those days are gone. The visual age brings all the elements together for us, and at a surprisingly low cost. Our library code, for example, must now communicate with the development environment; and we publish variables to the object inspector to set their values instead of hard-coding them. It’s not a big change--but it sure saves time.
In the first two articles of this series, we’ve covered the main issues surrounding component building. There’s more, though. Next time, we’ll add a Help file and build property and component editors for our component. Last--but certainly not least--we’ll use a custom icon.