Creating Custom Components

   

Creating Custom Components

The task of creating components can be quite daunting at first. After reading several articles and tutorials on the topic, it is quite easy to find yourself wondering where to start. The easiest approach is to start with a component that already exists and build on its features and capabilities.

As trivial as this might seem, you might just find yourself customizing or extending a number of the standard VCL components to suit the design and style of your real-world applications. While building a database application, you might drop a TDBGrid onto your form and change several properties to the same value. Similarly, while developing some in-house utilities, you always drop a TStatusBar onto your form, add some panels, and remove the size grip. Instead of doing this for every project, it would make sense to create your own custom component and have these properties set for you automatically. Not only does this make each new application faster to create, but you also have confidence that they are all bug free. Additionally, should you discover a new bug, all you have to do is correct the code in the component and recompile your package projects. They will all inherit the changes without any additional reprogramming.

Understanding Component Writing

There are different types of components; therefore, the ancestor of your own components will be determined by the very nature of that component.

Nonvisual components are derived from TComponent . TComponent is the minimal descendant that can be used for the creation of a component because it is the lowest -level component to offer the capability to be integrated into the IDE and have its properties streamed.

A nonvisual component is one that is simply a wrapper for other complex code in which there is no visual representation provided to the user . An example is a component that receives error log information and automatically sends it to a linked edit control, such as a TMemo or TrichEdit , and appends it to a file on disk. The component itself is invisible to the user of the application, but it continues to function in the background, providing the functionality required by the application.

Windowed components are derived from TWinControl . These objects appear to the user at runtime and can be interacted with (such as selecting a file from a list). Although it is possible to create your own components from TWinControl , C++Builder provides the TCustomControl component to make this task easier.

Graphic components are similar to windowed components with the main difference being that they don't have a window handle and, therefore, do not interact with the user. The absence of a handle also means fewer resources are being consumed. Although these components do not interact with the user, it is possible to have these components react to window messages such as those from mouse events. These components are derived from TGraphicControl .

Why Build on an Existing Component?

The biggest advantage you will find from building on existing components is the reduced development time of projects. It is also worthwhile to know that all the components used in your projects are bug free.

Take TLabel as an example, of which every project has more than one. If every project you created needed to maintain a particular design style, you could find yourself adding multiple label components and changing their properties to the same values for each new application. By creating a custom component descending from TLabel , you can add several of these new labels to a form and be left with only the task of setting their captions and positions .

To demonstrate how easy this can be, we can create a component in about a minute and have to type only three lines of code. From C++Builder's menu, choose Component, New Component. After the New Component dialog opens, select TLabel for the new component's Ancestor Type, and for the Class Name , type TStyleLabel . For a component that you will be installing into C++Builder's Component Palette and using in applications, you will probably want to choose a more descriptive class name. For this example, you could leave the other options with their default values and simply click the OK button. C++Builder will create the unit files for you; all that is needed is to add the lines of code that will set the label's properties. After you've made the necessary changes, save the file and from C++Builder's menu choose Component, Install Component. If you have the file open in C++Builder's IDE, the Unit File Name edit box will reflect the component's file. Click the OK button to install the component in the Component Palette. Listings 4.1 and 4.2 show the complete code.

Listing 4.1 The TStyleLabel Header File, StyleLabel.h
 //-------------------------------------------------------------------------- #include <SysUtils.hpp>  #include <Controls.hpp>  #include <Classes.hpp>  #include <Forms.hpp>  #include <StdCtrls.hpp>  //-------------------------------------------------------------------------- class PACKAGE TStyleLabel : public TLabel  {  private:  protected:  public:      __fastcall TStyleLabel(TComponent* Owner);  __published:  };  //-------------------------------------------------------------------------- #endif 
Listing 4.2 The TStyleLabel Code File, StyleLabel.cpp
 //--------------------------------------------------------------------------- #include <vcl.h>  #pragma hdrstop  #include "StyleLabel.h"  #pragma package(smart_init)  //--------------------------------------------------------------------------- // ValidCtrCheck is used to assure that the components created do not have  // any pure virtual functions.  //  static inline void ValidCtrCheck(TStyleLabel *)  {      new TStyleLabel(NULL);  }  //--------------------------------------------------------------------------- __fastcall TStyleLabel::TStyleLabel(TComponent* Owner)    : TLabel(Owner)  {      Font->Name = "Verdana";      Font->Size = 12;      Font->Style = Font->Style << fsBold;  }  //--------------------------------------------------------------------------- namespace Stylelabel  {      void __fastcall PACKAGE Register()      {           TComponentClass classes[1] = {__classid(TStyleLabel)};           RegisterComponents("TestPack", classes, 0);      }  }  //-------------------------------------------------------------------------- 

Another advantage to building on existing components is the ability to create a base class with all the functionality it requires, while leaving the properties unpublished. An example of this would be a specific TListBox type of component that doesn't have the Items property published to the user. By descending this component from TCustomListBox , it is possible to publish the properties you want the user to have access to (at design time), while making the others available (such as the Items property) only at runtime.

Finally, the properties and events you add to an existing component means writing far less code than if you create the component from scratch.

Designing Custom Components

Although it might seem trivial, the same rules apply to component design as per-application development when creating a custom component from an existing one. It is important to think about the possible future direction your components might take. The previously mentioned components that provide a list of database information don't just descend from TListBox . Instead, we decided to create a custom version of TCustomListBox that would contain the additional properties common to each descendant we wanted to create. Each new component was then built on this custom version, eliminating the need for three different versions of the same code. The final version of each component contained nothing more than the code (properties, methods , and events) that made it unique compared to its relatives.

Using the VCL Chart

To gain an appreciation for C++Builder's VCL architecture, take some time to review the VCL chart that ships with the product. This resource gives you a quick visual overview of not only what components are available, but also what they are derived from.

During your learning phase of component design and creation, you should endeavor to model your own components in this same object-oriented fashion, by creating strong, versatile base classes from which to create custom components. Although the source code for the C++Builder components are written in Pascal, it is a worthwhile exercise to look at each of the base classes for a particular component and see for yourself how they all come together. You will soon observe how components sharing the same properties are all derived from the same base class, or a descendant of one.

Finally, the chart shows what base classes are available for your own custom component requirements. In combination with the VCL help files, you can quickly determine the most suitable class from which to derive your components. As mentioned previously, the minimum base class will be TComponent , TWinControl , or TGraphicControl , depending on the type of component you will be creating.

Writing Nonvisual Components

The world of components is built on three main entities: properties, events, and methods. This section looks at each of these, with the aim of giving you a greater understanding of what makes up a component and how components work together to provide the building blocks of your C++Builder applications.

Properties

Properties come in two flavors: published and nonpublished. Published properties are available in the C++Builder Integrated Development Environment (IDE) at design time (they are also available at runtime). Nonpublished properties are used at runtime by your application. We will look at nonpublished properties first.

NonPublished Properties

A component is a packaged class with some additional functionality. Take a look at the sample class in Listing 4.3.

Listing 4.3 Getting and Setting Private Variables
 class LengthClass  {  private:      int FLength;  public:      LengthClass(void){}      ~LengthClass(void){}      int GetLength(void);      void SetLength(int pLength);      void LengthFunction (void);  } 

Listing 4.3 shows a private variable used internally by the class and the methods used by the application to read and write its value. This can easily lead to messy code. Take a look at Listing 4.4 for another example.

Listing 4.4 Using Set and Get Methods
 LengthClass Rope;  Rope.SetLength(15);  // do something  int NewLength = Rope.GetLength(); 

The code in Listing 4.4 isn't complex by any means, but it can quickly become difficult to read in a complex application. Wouldn't it be better if we could refer to Length as a property of the class? This is what C++Builder enables you to do. In C++Builder, the class could be written as shown in Listing 4.5.

Listing 4.5 Using a Property to Get and Set Private Variables
 class LengthClass2  {  private:      int FLength;  public:      LengthClass2(void){}      ~LengthClass2(void){}      void LengthFunction(void);      __property int Length = {read = FLength, write = FLength};  } 

The sample code in Listing 4.4 would be changed when using properties as shown in Listing 4.6.

Listing 4.6 Setting and Getting with a Property
 LengthClass Rope;  Rope.Length = 15;  // do something  int NewLength = Rope.Length; 

The class declaration has now been altered to use a __property (an extension to the C++ language in C++Builder). This property has read and write keywords defined. In Listing 4.6, when you read the Length property, you are returned the value of FLength ; when you set the Length property, you are setting the value of FLength .

Why go to all this trouble when you could just make the FLength variable public? Properties enable you to do the following:

  • You can make the Length property read-only by not using the write keyword.

  • You can provide an application public access to private information of the class without affecting the implementation of the class. This is more relevant when the property value is derived or some action needs to be taken when the value of the property changes.

  • You can cause side effects when the value is assigned to the property. These side effects can be used to maintain a consistent internal state for the object, to write information to a persistent store, or prepare other property values to be requested by a caller ( eager evaluation).

  • You can compute a value when it is asked for (lazy evaluation). This is especially nice for things such as infinite sequences of numbers (such as prime numbers ), or complex calculations that you might want to avoid performing if nothing ever requests the value.

Listing 4.7 shows a slight variation on the previous example.

Listing 4.7 Combining Set and Get Methods with Properties
 class LengthClass3  {  private:      int FLength;      int GetLength(void);      void SetLength(int pLength);  public:      LengthClass3(void){}      ~LengthClass3(void){}      void LengthFunction(void);      __property int Length = {read = GetLength, write = SetLength};  } 

The example in Listing 4.7 is starting to show how properties can become quite powerful. The property declaration shows that the value is returned by the GetLength() method when Length is read. The SetLength() method is called when Length needs to be set.

The GetLength() method might perform some calculations based on other private members of the class. The SetLength() method might perform some validation, and then continue to perform some additional tasks before finally setting the value of FLength .

In C++Builder, an example of this is the connection to a database source when the name of an alias is changed. As a developer, you change the name of the alias. In the background, the component is disconnecting from the current database (if there is one) before attempting to connect to the new source. The implementation is hidden from the user, but it is made available by the use of properties.

Types of Properties

Properties can be of any type, whether it is a simple data type such as int , bool , short , and so on, or a custom class. There are two considerations when using custom classes as property types. The first is that the class must be derived from TPersistent (at a minimum) if it is to be streamed to the form. The second is that, if you need to forward declare the class, you need to use the __declspec(delphiclass) keyword.

The code in Listing 4.8 will compile using typical forward declaration. Note that we haven't yet defined a property.

Listing 4.8 Forward Declaration
 class MyClass;  class PACKAGE MyComponent : public TComponent  {  private:      MyClass *FMyClass;  //   };  class MyClass : public TPeristent  {  public:       __fastcall MyClass (void){}  }; 

The PACKAGE keyword between the class name and class keyword is a macro that expands to code that enables the component to be exported from a package library ( .BPL ”Borland Package Library). A package library is a special kind of DLL that allows code to be shared between applications. For more information about package libraries and the PACKAGE macro, see "PACKAGE macro" and "Creating packages and DLLs" in the C++Builder online help.

But, if we want to add a property of type MyClass , we need to modify the forward declaration as shown in Listing 4.9.

Listing 4.9 Custom Class Property
 class __declspec(delphiclass) MyClass;  class PACKAGE MyComponent : public TComponent  {  private:      MyClass *FMyClass;  //   __published:      __property MyClass *Class1 = {read = FMyClass, write = FMyClass};  };  class MyClass : public TPeristent  {  public:      __fastcall MyClass (void){}  }; 
Published Properties

Publishing properties provides users with access to the properties of the component within the C++Builder IDE at design time. The properties are displayed in the Object Inspector, enabling the user to see or change the current value of those properties. The properties are also available at runtime, but their main purpose is to provide the user a quick method of setting up the component settings without the need to write a single line of code. Additionally, published properties are streamed to the form, so their values become persistent. This means the values are restored each time the project is opened and when the executable is launched.

Published properties are defined the same as all other properties, but they are defined in the __published area of the class declaration. Listing 4.10 shows an example.

Listing 4.10 Publishing a Property
 class PACKAGE LengthClass : public TComponent  {  private:      int FLength;      int GetLength(void);      void SetLength(int pLength);  public:      __fastcall LengthClass(TObject *Owner) : TComponent(Owner) {}      __fastcall ~LengthClass(void){}      void LengthFunction(void);  __published:      __property int Length = {read = Getlength, write = Setlength};  } 

The previous class is the same as in Listing 4.9 except that the Length property has been moved to the __published section. Published properties shown in the Object Inspector are readable and writeable , but it is possible to make a property read-only and still visible in the IDE by creating a dummy write method. Listing 4.11 shows how to add a published property in the previous component that shows the current version of the component.

Listing 4.11 A Version Property
 const int MajorVersion = 1;  const int MinorVersion = 0;  class PACKAGE LengthClass : public TComponent  {  private:      AnsiString FVersion;      int FLength;      int GetLength(void);      void SetLength(int pLength);      void SetVersion(AnsiString /* pVersion */)         {FVersion = AnsiString(MajorVersion) + "." +         AnsiString(MinorVersion);}  public:      __fastcall LengthClass(TObject *Owner) : TComponent(Owner)              {SetVersion("");}      __fastcall ~LengthClass(void){}      void LengthFunction(void);  __published:      __property int Length = {read = Getlength, write = Setlength};      __property AnsiString Version = {read = FVersion, write = SetVersion};  } 

We have defined a private variable FVersion , which has its value set in the class constructor. We have then added the Version property to the __published section and assigned the read and write keywords. The read keyword returns the value of Fversion , and the write method sets the value back to the original value. The variable name in the parameter list of SetVersion() has been commented out to prevent compiler warnings that the variable is declared, but not used. Because the property is of type AnsiString , the SetVersion() method by design must have an AnsiString parameter in the declaration.

Array Properties

Some properties are arrays, rather than simple data types such as bool , int , and AnsiString . This is not greatly documented for the user. An example of an array property is the Lines property of the TMemo component. This property enables the user to access the individual lines of the Memo component.

Array properties are declared the same as other properties, but with two main differences: The declaration includes the appropriate indexes with required types, and these indexes are not limited to being integers. Listings 4.12 through 4.15 illustrate the use of two properties. One takes a string as an index, and the other takes an integer value as an index.

Listing 4.12 Using a String as an Index
 class PACKAGE TStringAliasComponent : public TComponent  {  private:      TStringList RealList;      TStringList AliasList;      __AnsiString __fastcall GetStringAlias(AnsiString RawString);      AnsiString __fastcall GetRealString(int Index);      void __fastcall SetRealString(int Index, AnsiString Value);  public:       __property AnsiString AliasString[AnsiString RawString] =          {read = GetStringAlias};       __property AnsiString RealString[int Index] = {read=GetRealString,          write=SetRealString};  } 

The previous example could be part of a component that internally stores a list of strings and another list of alias strings. The AliasString property takes the RawString value and returns the alias via the GetStringAlias() method. The one thing many component writers are confused about when they first start using array properties is that the declaration uses index notation (that is, [] ), yet in code you use the same notation as when calling another method. Look at the RealString property, and notice that not only does it have an AnsiString return type, but it also takes an integer as an index. The GetRealString() method would be called when retrieving a particular string from the list based on the index, as in Listing 4.13.

Listing 4.13 Array Property Read Method
 AnsiString __fastcall TStringAliasComponent::GetRealString(int Index)  {      if(Index > (RealList->Count 1))          return "";      return RealList->Strings[Index];  } 

In code, the property would look like this:

 AnsiString str = StringAlias1->RealString[0]; 

Now take a look at the SetRealString() method. This method might look a bit odd if you are unfamiliar with using arrays as properties. It takes as its first parameter an integer value as its index and an AnsiString value. The RealList TStringList variable will insert the AnsiString in the list at the position specified by the index parameter. Listing 4.14 shows the definition of the SetRealString() method.

Listing 4.14 Array Property Write Method
 void __fastcall TStringAliasComponent::SetRealString(int Index,    AnsiString Value)  {      if((RealList->Count  1) < Index)          RealList->Add(Value);      else          RealList->Insert(Index, Value);  } 

In Listing 4.14, the value of the Index parameter is checked against the number of strings already in the list. If Index is greater, the string specified by Value is simply added to the end of the list. Otherwise , the Insert() method of TStringList is called to insert the string at the position specified by Index . Now you can assign a string to the list like this:

 StringAlias1->RealString[1] = "Some String"; 

Now here is the fun part. The GetStringAlias() method is the read method for the AliasString property, which takes a string as an index. You know that the string lists are arrays of strings, so every string has an index, or a position within the list. You can use the IndexOf() method of TStringList to compare the string passed as the index against the strings contained in the list. This method returns an integer value that is the index of the string within the list, or it returns -1 if the string is not present. Now all you have to do is return the string with the index returned from the call to IndexOf() from the list of aliases. This is demonstrated in Listing 4.15.

Listing 4.15 The GetStringAlias() Method
 AnsiString __fastcall TStringAliasComponent::GetStringAlias(AnsiString RawString)  {      int Index;      Index = RealList->IndexOf(RawString);      if((Index == -1)  (Index > (AliasList->Count-1)))          return RawString;      return AliasList->Strings [Index];  } 

To use the property, you would do something like this:

 AnsiString MyAliasString = StringAlias1->AliasString("The Raw String"); 
Beyond Read and Write

The code examples in Listings 4.5 through 4.15 have shown properties using read and write keywords as part of the declaration. C++Builder also provides three more options: default , nodefault , and stored .

The default keyword does not set the default value for the property. Instead, it tells C++Builder what default value will be assigned to this property (by the developer) in the component constructor. The IDE then uses this information to determine whether the value of the property needs to be streamed to the form. If the property is assigned a value equivalent to the default, the value of this property will not be saved as part of the form. For example

 __property int IntegerProperty = {read = Finteger, write = Finteger,    default = 10}; 

The nodefault keyword tells the IDE that this property has no default value associated with it. When a property is declared for the first time, there is no need to include the nodefault keyword because the absence of a default means there is no default. The nodefault keyword is mainly used when you need to change the definition of the inherited property. For example

 __property int DescendantInteger = {read = Finteger, write = Finteger,    nodefault}; 

Be aware that the value of a property with the nodefault keyword in its declaration will be streamed only if a value is assigned to the property or underlying member variable, either in one of its methods, or via the Object Inspector.

The stored keyword is used to control the storing of properties. All published properties are stored by default. You can change this behavior by setting the stored keyword to true or false or by giving the name of a function that returns a Boolean result. The code in Listing 4.16 shows an example of the stored keyword in use.

Listing 4.16 Using the stored Keyword
 class PACKAGE LengthClass : public TComponent  {  protected:      int FProp;      bool StoreProperty(void);  __published:      __property int AlwaysStore = {read = FProp, write = FProp, stored = true};      __property int NeverStore = {read = FProp, write = FProp, stored = false};      __property int SimetimesStore = {read = FProp, write = FProp,        stored = StoreProperty};  } 
Order of Creation

If your component has properties that depend on the values of other properties during the streaming phase, you can control the order in which they load (and hence initialize) by declaring them in the required order in the class header. For example, the code in Listing 4.17 loads the properties in the order PropA , PropB , PropC .

Listing 4.17 Property Dependencies
 class PACKAGE SampleComponent : public TComponent  {  private:      int FPropA;      bool FPropB;      String FProC;      void __fastcall SetPropB(bool pPropB);      void __fastcall SetPropC(String pPropC);  public:      __property int PropA = {read = FPropA, write = FPropA};      __property bool PropB = {read = FPropB, write = SetPropB};      __property String PropC = {read = FPropC, write = SetPropC};  } 

If you have properties with dependencies and are having trouble getting them to initialize correctly, ensure that the order of the property declarations in the class is correct.

Events

An event in a component is the call of an optional method in response to another incident. The incident could be a hook for the user to perform a task before the component continues the catching of an exception or the trapping of a Windows message.

As a simple example, let's assume we have a component that traverses directories from a given root location. If this component were designed to notify the user when the current directory has changed, this would be referred to as an event . When the event occurs, the component determines if the user has provided an event handler (a method attached to the event) and calls the respective method. If this all sounds confusing, take a look at Listing 4.18.

Listing 4.18 Declaring an Event Property
 class PACKAGE TTraverseDir : public TComponent  {  private:      AnsiString FCurrentDir;      TNotifyEvent *FOnDirChanged;  public:      __fastcall TTraverseDir(TObject *Owner) : TComponent(Owner){        FOnDirChanged = 0;}      __fastcall ~TTraverseDir(void){}      __fastcall Execute();  __published:      __property AnsiString CurrentDir = {read = FCurrentDir};      __property TNotifyEvent OnDirChanged = {read = FOnDirChanged,        write = FOnDirChanged};  } 

Listing 4.18 shows the relevant sections of code to describe the declaration of a read-only property and a standard event. When this component is executed, there will be instances when the current directory is changed. Let's have a look at some example code:

 void __fastcall TTraverseDir::Execute(void)  {  // perform the traversing of a directory  // This is where the directory has changed,  // call the DirChanged event if there is one.  if(FOnDirChanged)      FOnDirChanged(this);  // remainder of component code here  } 

The variable FOnDirChanged in the previous example is a pointer to a TNotifyEvent , which is declared as

 typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender) 

As you can see, the declaration indicates that a single parameter of type TObject* is expected. When the event is created (by double-clicking the event in the Object Inspector), the IDE creates the following code:

 void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender)  {  } 

Within this code, the user can now add code to be performed when this event is called. In this case, the event is a standard event that simply passes a pointer of the object that generated the event. This pointer enables you to distinguish between multiple components of the same type within the project.

 void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender)  {  if(Sender == Traverse1)      // perform this code for the component called Traverse1  else      // handle the alternative here  } 
How to Create an Event That Contains Additional Parameters

You will recall that the standard event is defined as shown in the following code:

 typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender) 

The following code shows how to define a custom event:

 typedef void __fastcall (__closure *TDirChangedEvent)     (System::TObject* Sender, bool &Abort) 

We have done two things in the previous code:

  • Created a unique typedef . TNotifyEvent is now TDirChangedEvent .

  • Added the required parameters to the parameter list.

We can now modify our class declaration. The changes are shown in Listing 4.19.

Listing 4.19 Custom Event Properties
 typedef void __fastcall (__closure *TDirChangedEvent)(System::TObject* Sender, bool &Abort)  class PACKAGE TTraverseDir : public TComponent  {  private:      TDirChangedEvent *FOnDirChanged;  __published:      __property TDirChangedEvent OnDirChanged = {read = FOnDirChanged,        write = FOnDirChanged};  } 

Now when the user creates the event, the IDE will add the following code:

 void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender, bool &Abort)  {  } 

There is only one more change to make: the source code that calls the event, as shown in Listing 4.20.

Listing 4.20 Calling the Event
 void __fastcall TTraverseDir::Execute(void)  {  // perform the traversing of a directory  bool &Abort = false;  // This is where the directory has changed,  // call the DirChanged event if there is one.  if(FOnDirChanged)      FOnDirChanged(this, Abort);  if(Abort)      // handle the abort process  // remainder of component code here  } 

The component has been sufficiently modified to enable the user to abort the process if required.

Methods

Methods of a component are supporting routines developed to carry out the various tasks required; they are no different than the methods defined for a typical class. In writing components, the goal is to minimize the number of methods the application needs to call. Here are some simple rules to follow when designing your components:

  • The user must not be required to call any methods to make the component behave the way he expects. For example, the component must take care of all initializations.

  • There must be no dependencies on the order in which given methods must be called. You must design your component to allow for any combination of events to take place. For example, if a user calls a routine that is state dependent (such as trying to query a database when there is no active connection), the component must handle the situation. Whether the component should attempt to connect or should throw an exception is up to the developer based on the component's function.

  • The user must not be able to call a method that would change the state of a component while it is performing another task.

  • The method should not generally be used to set or get values from the component because that is the role of the property.

Write your methods so that they check the current component state. If all of the requirements are not met, the component should attempt to correct the problem. The components should throw an exception if the component state cannot be corrected. Where appropriate, create custom exceptions so that the user can check for component-specific exception types.

Try to create properties rather than methods. Properties enable you to hide an active implementation from the user and, hence, make the component easier to understand.

Methods you write for components will typically be public or protected. Private methods should be written when they are hiding a specific implementation for that component, to the point that even derived components should not call them.

Public Methods

Public methods are those that the user needs to make the component perform as required.

When you have a method that runs for a long time, consider creating an event that can be used by the developer to inform the user of any processing activity taking place. Providing an opportunity for the user to abort the processing, for instance, through a return value from the event is another possibility.

Imagine a component that searches a tree of directories for a given file. Depending on the system being searched, this could take a great deal of processing time. Rather than leaving the user wondering if the application has ceased functioning, it is better to create an event that is called within the method. This event can then provide feedback, such as displaying the name of the current directory being traversed.

Protected Methods

If your components have methods that must not be called by the application developer, but need to be called from derived components, these methods are declared as protected . This ensures that the method is not called at the wrong time. It is safer to create public methods for the user that call protected methods when all requirements are established first.

When a method is created for the implementation of properties, it should be declared as a virtual protected method. This enables descendant components to enhance or replace the implementation used.

An example of a virtual protected method is the Loaded() method of components. When a component is completely loaded (streamed from the form), the Loaded() method is called.

In some cases, a descendant component needs to know when the component is loaded after all properties have been read so that it can perform some additional tasks. An example is a component that performs validation in a property setter, but cannot perform the validation until all properties have been read. In such a case, create a private variable called IsLoaded and set this to false in the constructor. (Although this is done by default, doing it this way makes the code more readable.) Then, overload the Loaded() method and set IsLoaded to true . This variable can then be used in the property-implementation methods to perform validation as required.

Listings 4.21 and 4.22 are from the custom TAliasComboBox component. TAliasComboBox is part of the free MJFPack package, which can be downloaded from http://www.mjfreelancing.com. The package contains other components that can be linked together in this fashion.

Listing 4.21 The TAliasComboBox Header File
 class PACKAGE TAliasComboBox : public TSmartComboBox  {  private:      bool IsLoaded;  protected:      virtual void __fastcall Loaded(void);  } 
Listing 4.22 The TAliasComboBox Source File
 void __fastcall TAliasComboBox: :Loaded(void)  {  TComponent::Loaded();  if(!ComponentState.Contains(csDesigning))      {      IsLoaded = true;      GetAliases();      }  } 

In this code, you can see that the Loaded() method has been overloaded in the class declaration. In the .CPP file, start by calling the ancestor Loaded() method, and then your additional code. Listing 4.22 shows the component verifying that it is not in design mode before it retrieves available alias information. Because the state of certain properties might depend on other properties, additional methods for this component check the IsLoaded variable before performing any processing that might require the value of those properties to be set. Essentially , most of the processing by this component is performed only at runtime.

Creating Component Exceptions

Sometimes it is possible to rethrow an exception that you have caught in your component, which enables the user to deal with the situation. You have more than likely performed a number of steps in your component that need to be cleaned up when an exception occurs. After you have performed the cleanup process, you need to do one of two things.

First, you can rethrow the exception. This would be the standard approach for an error such as Divide By Zero . However, there are situations in which it would be better to convert the exception into an event. This provides very clean handling methods for your users. Don't make the mistake of converting all exceptions to events because this can sometimes make it harder for your users to develop their applications.

An example might help to make this clearer. Imagine a component performing a number of sequential database queries. This component would be made up of a TStrings property that contains all the queries and an Execute() method that performs them. How does the user want to use this component? Something such as the following would be the most desirable.

 MultiQuery->Queries->Assign(Memo1->Lines);  MultiQuery1->Execute(); 

This is very simple code for the user to implement, but what about a possible exception? Should the user be required to handle any exceptions himself? This might not be the best approach during one of the queries. A better approach would be to build an event that is called when an exception occurs. Within the event, the user should have the opportunity to abort the process.

Let's create a custom exception that will be called if the user attempts to execute an individual query when it is outside the available index range. For the moment, assume that there is another method called ExecuteItem() that takes an index to the list of available queries.

First, we need to create the exception in the header file. This is as simple as creating a new exception class derived from the Exception class, as shown in Listing 4.23.

Listing 4.23 A Custom Exception Class
 class EMultiQueryIndexOutOfBounds : public Exception  {  public:     __fastcall EMultiQueryIndexOutOfBounds(const AnsiString Msg) :       Exception(Msg){}  }; 

That's it. Now if the user tries to execute a query (by index), and the index provided is outside the available range, we can throw our unique exception.

The code for throwing this exception is shown in Listing 4.24.

Listing 4.24 Throwing the Custom Exception
 void __fastcall TMultiQuery::ExecuteItem(int Index)  {  if(Index < 0  Index > Queries->Count)      throw EmultiQueryIndexOutOfBounds;  //  perform the query here  } 

As you can see from Listings 4.23 and 4.24, a custom exception is very easy to create and implement. If this component is to perform the query at design time, you need to provide the user with a message (rather than have an exception thrown within the IDE). You should modify the code as shown in Listing 4.25.

Listing 4.25 Throwing an Exception at Design Time
 void __fastcall TMultiQuery::ExecuteItem(int Index)  {  if(Index < 0  Index > Queries->Count)      {      if(ComponentState.Contains(csDesigning))          throw EmultiQueryIndexOutOfBounds("The Query index is out of range");      else          throw EmultiQueryIndexOutOfBounds;      }  //  perform the query here  } 
The namespace

As you develop your components and name them, there might be other developers who, by coincidence , use the same names . This will cause conflicts when using both components in the same project. This is overcome with the namespace keyword.

When a component is created using the New Component Wizard, the IDE creates code similar to that shown in Listing 4.26.

Listing 4.26 namespace Code
 namespace Aliascombobox  {      void __fastcall PACKAGE Register()      {          TComponentClass classes[1] = {__classid(TAliasComboBox)};          RegisterComponents("MJF Pack", classes,  0);      }  } 

The namespace keyword ensures that the component is created in its own subsystem. Let's look at a case where namespace needs to be used even further within a package.

Suppose that two developers build a clock component, and they both happen to create a const variable to indicate the default time mode. If both clocks are used in an application, the compiler will complain because of the duplication.

 // From the first developer  const bool Mode12;   // 12 hour mode by default  class PACKAGE TClock1 : public TComponent    {    }  // From the second developer  const bool Mode12;   // 12 hour mode by default  class PACKAGE TClock2 : public TComponent    {    } 

As you can see, it is important to develop your component packages with this possibility in mind. To get around this issue, use the namespace keyword. After all the #include statements in your header file, surround the code as shown in Listing 4.27.

Listing 4.27 Surrounding Your Code
 namespace NClock1  {  class PACKAGE TClock1 : public    }  } 

Develop a convention for all your components. For example, you could start your namespace identifiers with a capital N , followed by the component name. If it is possible that the same name has already been used, come up with something unique, such as prefixing with your company's initials . Using namespaces in this fashion ensures that your packages will integrate smoothly with others.

Responding to Messages

The VCL does a fantastic job of handling almost all of the window messages you will ever require. There are times, however, when a need arises to respond to an additional message to further enhance your project.

NOTE

Keep in mind that the explicit use of Windows messages will prevent porting your component to other operating systems. CLX components you create should never use Windows messages directly.


An example of such a requirement is to support filename drag and drop from Windows Explorer onto a string Grid component. We can create such a component, called TSuperStringGrid , that is nothing more than a descendant of TStringGrid with some additional functionality.

The drag-and-drop operation is handled by the API message WM_DROPFILES . The information needed to carry out the operation is stored in the TWMDropFiles structure.

The interception of window messages in components is the same as for other areas of your projects. The only difference is that we are working with a component and not with the form of a project. Hence, we set up a message map, as shown in Listing 4.28.

Listing 4.28 Trapping Messages
 BEGIN_MESSAGE_MAP     MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles)  END_MESSAGE_MAP(TStringGrid) 

NOTE

No trailing semicolons areused in declaring the message map. This is because BEGIN_MESSAGE_MAP , MESSAGE_HANDLER , and END_MESSAGE_MAP are macros that expand to code during compilation. The macros contain the necessary semicolons.


The code in Listing 4.28 creates a message map for the component (note TStringGrid in the END_MESSAGE_MAP macro). The message handler will pass all intercepts of the WM_DROPFILES messages to the WmDropFiles() method (which will be created shortly). The information is passed to this method in the TWMDropFiles structure as defined by Windows.

Now we need to create the method that will handle the message. In the protected section of the component we define the method as shown in the following code:

 protected:     void __fastcall WmDropFiles(TWMDropFiles &Message); 

You'll notice we have provided a reference to the required structure as a parameter of the method.

Before this component will work, we need to register the component with Windows, telling it that the string grid is allowed to accept the dropped filenames. This is performed when the component is loaded via the DragAcceptFiles() command.

 DragAcceptFiles(Handle, FCanDropFiles); 

In the previous code, the FCanDropFiles variable is used by the component to indicate whether it is allowed to accept the filenames as part of a drag-and-drop operation.

Now the method accepts the filenames when the component intercepts the Windows message. The code in Listing 4.29 is stripped slightly from the full version.

Listing 4.29 Accepting Dropped Files
 void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message)  {  char buff[MAX_PATH];  HDROP hDrop = (HDROP)Message.Drop;  POINT Point;  int NumFiles = DragQueryFile(hDrop, -1, NULL, NULL);  TStringList *DFiles = new TStringList;  DFiles->Clear();  DragQueryPoint(hDrop, &Point);  for(int you = 0; you < NumFiles; i++)      {      DragQueryFile(hDrop, i, buff, sizeof(buff));      DFiles->Add(buff);      }  DragFinish(hDrop);  // do what you want with the list of files now stored in DFiles  delete DFiles;  } 

An explanation of this code is beyond the scope of this chapter. The help files supplied with C++Builder provide a good overview of what each function performs.

As you can see, intercepting messages is not hard after you understand how to set them up, although some understanding of the Windows API is required. Refer to the messages.hpp file that comes with your C++Builder installation for a list of the message structures available.

Design Time Versus Runtime

We've already made some references to the operation of a component at design time compared to runtime. Design time operation refers to how the component behaves while the user is creating the project in the IDE. Runtime operation refers to what the component does when the application is executed.

The TComponent object has a property (a Set ) called ComponentState that is made up of the following constants: csAncestor , csDesigning , csDesignInstance , csDestroying , csFixups , csFreeNotification , csInline , csLoading , csReading , csWriting , and csUpdating . Table 4.1 lists these ComponentState flags and gives the purpose of each.

Table 4.1. The ComponentState Flags

Flag

Purpose

csAncestor

Indicatesthat the component was introduced in an ancestor form. Set only if csDesigning is also set. Set or cleared in the TComponent::SetAncestor() method.

csDesigning

Indicatesthat the component is being manipulated at design time. Used to distinguish design time and runtime manipulation. Set or cleared in the TComponent::SetDesigning() method.

csDesignInstance

Indicates that the component is the root object in a designer. For example, it is set for a frame when you are designing it, but not on a frame that acts like a component. This flag always appears with csDesigning . Set or cleared in the TComponent::SetDesignInstance() method.

csDestroying

Indicatesthat the component is being destroyed . Set in the TComponent::Destroying() method.

csFixups

Indicatesthat the component is linked to a component in another form that has not yet been loaded. This flag is cleared when all pending fixups are resolved. Cleared in the GlobalFixupReferences() global function.

csFreeNotification

Indicatesthat the component has sent a notification to other forms that it is being destroyed, but has not yet been destroyed. Set in the TComponent::FreeNotification() method.

csInline

Indicatesthat the component is a top-level component that can be modified at design time and also embedded in a form. This flag is used to identify nested frames while loading and saving. Set or cleared in the component's SetInline() method. Also set in the TReader::ReadComponent() method

csLoading

Indicates that a filer object is currently loading the component. This flag is set when the component is first created and not cleared until the component and all its children are fully loaded (when the Loaded() method is called). Set in the TReader::ReadComponent() and TReader::ReadRootComponent() methods. Cleared in the TComponent::Loaded() method. (For more information on filer objects, see "TFiler" in the C++Builder online help index.)

csReading

Indicates that the component is reading its property values from a stream. Note that the csLoading flag is always set when csReading is set. That is, csReading is set for the period of time that a component is reading in property values when the component is loading. Set and cleared in the TReader::ReadComponent() and TReader::ReadRootComponent() methods.

csWriting

Indicatesthat the component is writing its property values to a stream. Set and cleared in the TWriter::WriteComponent() method.

csUpdating

Indicatesthat the component is being updated to reflect changes in an ancestor form. Set only if csAncestor is also set. Set in the TComponent::Updating() method andcleared in the TComponent::Updated() method.

The Set member we are most interested in is csDesigning . As long as the component exists in the IDE (as part of a developing project), the component will contain this constant as part of the Set to indicate that it is being used at design time. To determine if a component is being used at design time, use the following code:

 if(ComponentState.Contains(csDesigning))      // carry out the designtime code here  else       // carry out the runtime code here 

Why would you need to run certain code at runtime only? This is required in many instances, such as the following:

  • To specifically validate a property that has dependencies available only at runtime.

  • To display a warning message to the user if he sets an inappropriate property value.

  • To display a selection dialog or a property editor if an invalid property value is given.

Many component writers don't go to the trouble of providing the user with these types of warnings and dialogs. However, it is these extra features that make a component more intuitive and user friendly.

Linking Components

Linking components refers to giving a component the capability to reference or alter another component in the same project. An example in C++Builder is the TDriveComboBox component. This component has a property called DirList that enables the developer to select a TDirectoryListBox component available on the same form. This type of link gives the developer a quick and easy method to update the directory listing automatically every time the selected drive is changed. Creating a project to display a list of directories and filenames doesn't get any easier than dropping three components ( TDriveComboBox , TdirectoryListBox , and TFileListBox ) onto a form and setting two properties. Of course, you still need to assign code to the event handlers to actually make the project perform something useful, but up to that point there isn't a single line of code to be written.

Providing a link to other components starts by creating a property of the required type. If you create a property of type TLabel , the Object Inspector will show all available components on the form that are of type TLabel . To show how this works for descendant components, we are going to create a simple component that can link to a TMemo or a TRichEdit component. To do this, you need to realize that both components descend from TCustomMemo .

Let's start by creating a component descending from TComponent that has a property called LinkedEdit , as shown in Listing 4.30.

Listing 4.30 Linked Components
 class PACKAGE TMsgLog : public TComponent  {  private:      TCustomMemo *FLinkedEdit; // can be TMemo or TRichEdit or any other derived component  public:      __fastcall TMsgLog(TComponent* Owner);      __fastcall ~TMsgLog(void);      void __fastcall OutputMsg(const AnsiString Message);  protected:      virtual void __fastcall Notification(TComponent *AComponent,        TOperation Operation);  __published:      __property TCustomMemo *LinkedEdit = {read = FLinkedEdit,        write = FLinkedEdit};  }; 

The code in Listing 4.30 creates the component with a single property, called LinkedEdit . There are two more things to take care of. First, we need to output the messages to the linked Memo or RichEdit component (if there is one). We also need to take care of the possibility that the user might delete the linked edit control. The OutputMsg() method is used to pass the text message to the linked edit control, and the Notification() method is used to detect if it has been deleted.

The following provides the output:

 void __fastcall TMsgLog::OutputMsg(const AnsiString Message)  {  if(FLinkedEdit)      FLinkedEdit->Lines->Add(Message);  } 

Because both TMemo and TRichEdit components have a Lines property, there is no need to perform any casting. If you need to perform a task that is component specific (or handled differently), use the code shown in Listing 4.31.

Listing 4.31 The OutputMsg() Method
 void __fastcall TMsgLog::OutputMsg(const AnsiString Message)  {  TMemo *LinkedMemo = 0;  TRichEdit *LinkedRichEdit = 0;  LinkedMemo = dynamic_cast<TMemo *>(FLinkedEdit);  LinkedRichEdit = dynamic_cast<TRichEdit *>(FLinkedEdit);  if(FLinkedMemo)      FLinkedMemo->Lines->Add(Message);  else      {      FLinkedRichEdit->Font->Color = clRed;      FLinkedRichEdit->Lines->Add(Message);      }  } 

The final check is to detect the linked edit control being deleted. This is done by overloading the Notification() method of Tcomponent , as shown in Listing 4.32.

Listing 4.32 The Notification() Method
 void __fastcall TMsgLog::Notification(TComponent *AComponent,    TOperation Operation)  {  // We don't care about controls being added.  if(Operation != opRemove)      return ;  // We have to check each one in case the user did something  // like have the same label attached to multiple properties.  if(AComponent == FLinkedEdit)      FLinkedEdit = 0;  } 

The code in Listing 4.32 shows how to handle code resulting from another component being deleted. The first two lines are to show the purpose of the Operation parameter.

The most important code is the last two lines, which compare the pointer AComponent to the LinkedEdit property (a pointer to a component descending from TCustomMemo ). If the pointers match, we NULL the LinkedEdit pointer. This removes the reference from the Object Inspector and ensures that our code is no longer pointing to a memory address that is about to be lost (when the edit component is actually deleted). Note that LinkedEdit = 0 is the same as LinkedEdit = NULL .

One final point is that if you link your component to another that has dependencies (such as TDBDataSet descendants that require a database connection), it is up to you to ensure that these dependencies are checked and handled appropriately. Good component design is recognized when the user has the least amount of work to do to get the component to behave as expected.

Linking Events Between Components

We've looked at how components can be linked together via properties. Our discussion so far has been about how a property of TMsgLog can be linked to another component so that messaging can be provided automatically without the user having to write the associated code.

What we are going to look at now is how to link events between components. Continuing with the previous examples, we're going to show how we intercept the OnExit event for the linked edit control (note that TMemo and TRichEdit both have an OnExit event and are of type TNotifyEvent ) so that we can perform some additional processing after the user's code has executed. Let's assume the linked edit control is not read-only. This means the user could enter something into the log; this change needs to be recorded as a user-edited entry. We will demonstrate how to perform the intercept and leave the functionality up to you.

Component events can be implemented differently according to the nature of the event itself. If the component is looping through a process, the code might simply have a call to execute the event handler if one exists. Take a look at the following example:

 // start of loop  if(FOnExit)      FOnExit(this);  endif;  //   // end of loop 

Other events could result from a message. Listing 4.26 showed the message map macro for accepting files dropped onto a control from Windows Explorer as follows :

 BEGIN_MESSAGE_MAP     MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles)  END_MESSAGE_MAP(TStringGrid) 

If our component has an OnDrop event, we can write our implementation as shown in the following code:

 void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message)  {  if(FOnDrop)      FOnDrop(this);  endif;  //  remainder of code here  } 

What you should have noticed by now is that the components maintain a pointer to the event handler, such as FOnExit and FOnDrop in the previous example. This makes it very easy to create our own pointer to note where the user's handler resides, and then redirect the user's event so that it calls an internal method instead. This internal method will execute the user's original code, followed by the component's code (or vice versa).

The only other consideration to make is when you redirect the pointers. The logical place to do this is in the component's Loaded() method. This is called when the entire component is streamed from the form, and, hence, all of the user's event handlers have been assigned.

Define the Loaded() method and a pointer to a standard event in your class. (The event is the same type as the one we are going to intercept ”in our case it is the OnExit event, which is of type TNotifyEvent .) We also need an internal method with the same declaration as the event that we are intercepting. In our class, we create a method called MsgLogOnExit . This is the method that will be called before the OnExit event of the linked edit control. In Listing 4.33, we include a typedef of type TComponent called Inherited . The reason will become obvious when we get to the source code.

Listing 4.33 The TMsgLog Class Header File
 class PACKAGE TMsgLog : public TComponent  {  typedef TComponent Inherited;  private:      TNotifyEvent *FonUsersExit;      void __fastcall MsgLogOnExit(TObject *Sender);  protected:      virtual void __fastcall Loaded(void);  //  remainder of code not shown  } 

In the source code, you might have something such as Listing 4.34.

Listing 4.34 The TMsgLog Class Source File
 void __fastcall TMsgLog::TMsgLog(TComponent *Owner)  {  FOnUsersExit = 0;  }  void __fastcall TMsgLog::Loaded(void)  {  Inherited::Loaded();  if(!ComponentState.Contains(csDesigning))      {      if(FlinkedEdit)          {          if(FlinkedEdit->OnExit)              FOnUsersExit = FlinkedEdit->OnExit;          FlinkedEdit->OnExit = MsgLogOnExit;          }      }  }  void __fastcall TMsgLog::MsgLogOnExit(TObject *Sender)  {  if(FOnUsersExit)      FOnUsersExit(this);  //  and now perform the additional code we want to do  } 

When the component is first created, the constructor initializes FOnUsersExit to NULL . When the form is completely streamed, the component's OnLoaded event is called. This starts by calling the inherited method first (the typedef simply helps to make the code easy to read). Next , we make sure the component is not in design mode. If the application is in runtime mode, we see if the component has a linked edit control. If so, we find out if the user has assigned a method to the OnExit event of that control. If these tests are true , we set our internal pointer FOnUsersExit to the address of the user's event handler. Finally, we reassign the edit control's event handler to our internal method MsgLogOnExit() . This results in the MsgLogOnExit() method being called every time the cursor exits the edit control, even if the user did not assign an event handler.

The MsgLogOnExit() method starts by determining if the user assigned an event handler; if so, it is executed. We then continue to perform the additional processing tasks we want to implement. The decision to call the user's event before or after our own code is executed depends on the nature of the event, such as data encryption or validation.

Writing Visual Components

As you've seen, components can be any part of a program that the developer can interact with. Components can be nonvisual ( TOpenDialog or TTable ) or visual ( TListBox or TButton ). The most obvious difference between them is that visual components have the same visual characteristics during design time as they do during runtime. As the properties of the component that determine its visual appearance are changed in the Object Inspector, the component must be redrawn or repainted to reflect those changes. Windowed controls are wrappers for Windows Common Controls, and Windows will take care of redrawing the control more often than not. In some situations, such as with a component that is not related to any existing control, redrawing the component is up to you. In either case, it is helpful to know some of the useful classes that C++Builder provides for drawing onscreen.

Where to Begin

One of the most important considerations when writing components is determining the parent class from which to inherit. You should review the help files and the VCL source code if you have it. This is time well spent; there is nothing more frustrating than having worked on a component for hours or days just to discover that it doesn't have the capabilities you need. If you are writing a windowed component (one that can receive input focus and has a window handle), derive it from TCustomControl or TWinControl . If your component is purely graphical, such as a TSpeedButton , derive from TGraphicControl . Very few if any limitations exist when it comes to writing visual components, and there is a wealth of freeware and shareware components and source code on the Internet from which to get ideas. http://www.torry.net/ is one of the most comprehensive sources; others can be found on the C++Builder Programmer's Webring, which starts on http://www.temporaldoorway.com/programming/cbuilder/index.htm.

TCanvas

The TCanvas object is C++Builder's wrapper for the Device Context. It encapsulates various tools for drawing complex shapes and graphics onscreen. TCanvas can be accessed through the Canvas property of most components, although Windows draws some windowed controls and, therefore, those windowed controls do not provide a Canvas property. There are ways around this, and we'll discuss them shortly. TCanvas also provides several methods to draw lines, shapes, and complex graphics onscreen.

Listing 4.35 is an example of how to draw a line diagonally from the upper-left corner to the bottom-right corner of the canvas. The LineTo() method draws a line from the current pen position to the coordinates specified in the X and Y variables. First, set the start position of the line by calling the MoveTo() method.

Listing 4.35 Drawing a Line Using MoveTo()
 Canvas->MoveTo(0, 0);  int X = ClientRect.Right;  int Y = ClientRect.Bottom;  Canvas->LineTo (X, Y); 

Listing 4.36 uses the Frame3D() method to draw a frame around a canvas, giving the control a button appearance.

Listing 4.36 Creating a Button Appearance
 int PenWidth = 2;  TColor Top = clBtnHighlight;  TColor Bottom = clBtnShadow;  Frame3D(Canvas, ClientRect, Top, Bottom, PenWidth); 

It is also very common to use API drawing routines with the TCanvas object to accomplish certain effects. Some API drawing methods use the DeviceContext of the control, although it isn't always necessary to get the HDC of the control to call an API that requires it. To get the HDC of a control, use the GetDC() API.

NOTE

HDC is the data type returned by the call to GetDC() . It is simply the handle of the DeviceContext and is synonymous with the Handle property of TCanvas .


Listing 4.37 uses a form with TPaintBox (we'll use TPaintBox because its Canvas property is published) and calls the RoundRect() API to draw an ellipse within the TPaintBox . The TPaintBox can be placed anywhere on the form. The code would be placed in the OnPaint event handler for the TPaintBox . The full project can be found in the PaintBox1 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr .

Listing 4.37 Using API Drawing Methods
 void __fastcall TForm1::PaintBox1Paint(TObject *Sender)  {      // We'll use a TRect structure to save on typing      TRect Rect;      int nLeftRect, nTopRect, nRightRect, nBottomRect, nWidth, nHeight;      Rect = PaintBox1->ClientRect;      nLeftRect = Rect.Left;      nTopRect = Rect.Top;      nRightRect = Rect.Right;      nBottomRect = Rect.Bottom;      nWidth = Rect.Right - Rect.Left;      nHeight = Rect.Bottom - Rect.Top;      if(RoundRect(PaintBox1->Canvas->Handle, // handle of device context          nLeftRect,  // x-coord. of bounding rect's upper-left       corner          nTopRect,     // y-coord. of bounding rect's upper-left corner          nRightRect,  // x-coord. of bounding rect's lower-right corner          nBottomRect,  // y-coord. of bounding rect's lower-right corner          nWidth,  // width of ellipse used to draw rounded corners          nHeight  // height of ellipse used to draw rounded corners) == 0)          ShowMessage("RoundRect failed...");  } 

Try changing the values of the nWidth and nHeight variables. Start with zero; the rectangle will have sharp corners. As you increase the value of these two variables, the corners of the rectangle will become more rounded. This method and other similar drawing routines can be used to create buttons or other controls that are rounded or elliptical . Some examples will be shown later. See "Painting and Drawing Functions" in the Win32 help files (win32.hlp, specifically) that ship with C++Builder for more information.

Using Graphics in Components

Graphics are becoming more commonplace in components. Some familiar examples are TSpeedButton and TBitButton , and there are several freeware, shareware, and commercial components available that use graphics of some sort . Graphics add more visual appeal to components and, fortunately, C++Builder provides several classes to handle bitmaps, icons, JPEGs, and GIFs. The norm for drawing components is to use an offscreen bitmap to do the drawing, and then copy the bitmap to the onscreen canvas. This reduces screen flicker because the canvas is painted only once. This is very useful if the image you are working with contains complex shapes or images. The TBitmap class has a Canvas property, which is a TCanvas object and, thus, enables you to draw shapes and graphics off the screen.

The following example uses a form with a TPaintBox component. A TBitmap object is created and used to draw an image similar to a TSpeedButton with its Flat property set to true . The TBitmap is then copied to the screen in one action. In this example we add a TButton , which will change the appearance of the image from raised to lowered . The full project can be found in the PaintBox2 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr . First, take a look at the header file in Listing 4.38.

Listing 4.38 Creating a Raised or Lowered Appearance
 class TForm1 : public TForm  {  __published: // IDE-managed Components      TPaintBox *PaintBox1;      TButton *Button1;  private:      // User declarations      bool IsUp;  public:               // User declarations      __fastcall TForm1(TComponent* Owner);  }; 

We declare a Boolean variable IsUp , which we'll use to swap the highlight and shadow colors and to change the caption of the button. If IsUp is true , the image is in its up state; if the value of IsUp is false , the image is in its down state. Because IsUp is a member variable, it will be initialized to false when the form is created. The Caption property of Button1 can be set to Up via the Object Inspector.

The OnClick event of the button is quite simple. It changes the value of the IsUp variable, changes the Caption property of the button based on the new value, and calls the TPaintBox 's Repaint() method to redraw the image. This is shown Listing 4.39.

Listing 4.39 The Button1Click() Method
 void __fastcall TForm1::Button1Click(TObject *Sender)  {      IsUp = !IsUp;      Button1->Caption = (IsUp) ? "Down" : "Up";      PaintBox1->Repaint ();  } 

A private method, SwapColors() , is declared and will change the highlight and shadow colors based on the value of the IsUp variable, which is shown in Listing 4.40.

Listing 4.40 The SwapColors() Method
 void __fastcall TForm1::SwapColors(TColor &Top, TColor &Bottom)  {      Top = (IsUp) ? clBtnHighlight : clBtnShadow;      Bottom = (IsUp) ? clBtnShadow :  clBtnHighlight;  } 

The final step is to create an event handler for the OnPaint event of the TPaintBox . This is shown in Listing 4.41.

Listing 4.41 Painting the Button
 void __fastcall TForm1::PaintBox1Paint(TObject *Sender)  {      TColor TopColor, BottomColor;      TRect Rect;      Rect = PaintBox1->ClientRect;      Graphics::TBitmap *bit = new Graphics::TBitmap;      bit->Width = PaintBox1->Width;      bit->Height = PaintBox1->Height;      bit->Canvas->Brush->Color = clBtnFace;      bit->Canvas->FillRect(Rect);      SwapColors(TopColor, BottomColor);      Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);      PaintBox1->Canvas->Draw(0, 0, bit);      delete bit;  } 

Listing 4.42 will go one step further and demonstrate how to use bitmap files as well as drawing lines on a canvas. Most button components, for example, contain not only lines and borders that give it shape, but also icons, bitmaps, and text. This can become a bit more complicated because it requires a second TBitmap to load the graphics file, the position of the graphic must be calculated and copied to the first bitmap, and the final result must be copied to the onscreen canvas. The full project can be found in the PaintBox3 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr .

Listing 4.42 Using Bitmaps and Lines
 void __fastcall TForm1::PaintBox1Paint(TObject *Sender)  {      TColor TopColor, BottomColor;      TRect Rect, gRect;      Rect = PaintBox1->ClientRect;      Graphics::TBitmap *bit = new Graphics::TBitmap;      Graphics::TBitmap *bitFile = new Graphics::TBitmap;      bitFile->LoadFromFile("geom1b.bmp");      // size the off-screen bitmap to size of on-screen canvas      bit->Width = PaintBox1->Width;      bit->Height = PaintBox1->Height;      // fill the canvas with the brush's color      bit->Canvas->Brush->Color = clBtnFace;      bit->Canvas->FillRect(Rect);      // position the second TRect structure centered h/v within Rect      gRect.Left = ((Rect.Right - Rect.Left) / 2) - (bitFile->Width / 2);      gRect.Top = ((Rect.Bottom - Rect.Top) / 2) - (bitFile->Height / 2);      // move the inner rect up and over by 1 pixel to give the appearance of      // the panel moving up and down      gRect.Top += (IsUp) ? 0 : 1;      gRect.Left += (IsUp) ? 0 : 1;      gRect.Right = bitFile->Width + gRect.Left;;      gRect.Bottom = bitFile->Height + gRect.Top;      // copy the bitmap to the off-screen bitmap object using transparency      bit->Canvas->BrushCopy(gRect, bitFile,        TRect(0,0,bitFile->Width, bitFile->Height), bitFile->TransparentColor);      // draw the borders      SwapColors(TopColor, BottomColor);      Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);      // copy the off-screen bitmap to the on-screen canvas      BitBlt(PaintBox1->Canvas->Handle, 0, 0,    PaintBox1->ClientWidth,        PaintBox1->ClientHeight, bit->Canvas->Handle,    0, 0,   SRCCOPY);      delete bitFile;      delete bit;  } 
Responding to Mouse Messages

Graphical components are normally derived from TGraphicControl , which provides a canvas to draw on and handles WM_PAINT messages. Remember that nonwindowed components do not need to receive input focus and do not have or need a window handle. Although these types of components cannot receive input focus, the VCL provides custom messages for mouse events that can be trapped.

For example, when the Flat property of a TSpeedButton is set to true , the button pops up to show its borders when the user moves the mouse cursor over it, and it returns to a flat appearance when the mouse is moved away from the button. This effect is accomplished by responding to two messages ” CM_MOUSEENTER and CM_MOUSELEAVE , respectively. These messages are shown in Listing 4.43.

Listing 4.43 The CM_MOUSEENTER and CM_MOUSELEAVE Messages
 void __fastcall CMMouseEnter(TMessage &Msg);  // CM_MOUSEENTER  void __fastcall CMMouseLeave(TMessage &Msg);  // CM_MOUSELEAVE  BEGIN_MESSAGE_MAP    MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)    MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)  END_MESSAGE_MAP(TBaseComponentName) 

Another important message to consider is the CM_ENABLEDCHANGED message. The Enabled property of TGraphicControl is declared as public, and the setter method simply sends the control the CM_ENABLECHANGED message so that the necessary action can be taken; for example, showing text or graphics as grayed or not firing an event. If you want to give your component the capability to be enabled or disabled, you would redeclare this property as published in your component's header file and declare the method and message handler. Without it, users will still be able to assign a true or false value to the Enabled property at runtime, but it will have no effect. The CM_ENABLECHANGED message is shown in Listing 4.44.

Listing 4.44 The CM_ENABLEDCHANGED Message
 void __fastcall CMEnabledChanged(TMessage &Msg);  __published:      __property Enabled ;  BEGIN_MESSAGE_MAP    MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)  END_MESSAGE_MAP(TYourComponentName) 

Other mouse events such as OnMouseUp , OnMouseDown , and OnMouseOver are conveniently declared in the protected section of TControl , so all that is necessary is to override the methods to which you want to respond. If you want derivatives of your component to have the capability to override these events, remember to declare them in the protected section of the component's header file. This is shown in Listing 4.45.

Listing 4.45 Overriding TControl 's Mouse Events
 private:      TMmouseEvent FOnMouseUp;      TMouseEvent FOnMouseDown;      TMouseMoveEvent FOnMouseMove;  protected:      void __fastcall MouseDown(TMouseButton Button, TShiftState Shift, int X,        int Y);      void __fastcall MouseMove(TshiftState Shift, int X, int Y);      void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X,        int Y);  __published:      __property TMouseEvent OnMouseUp = {read=FOnMouseUp, write=FOnMouseUp};      __property TMouseEvent OnMouseDown = {read=FOnMouseDown,        write=FOnMouseDown};      __property TMouseMoveEvent OnMouseMove = {read=FOnMouseMove,        write=FOnMouseMove}; 

In the example projects shown previously, an event handler was created for the OnPaint() event of TPaintBox . This event is fired when the control receives the WM_PAINT message. TGraphicControl traps this message and provides a virtual Paint() method that can be overridden in descendant components to draw the control onscreen or, as TPaintBox does, provide an OnPaint() event.

These messages and others are defined in messages.hpp . If you have the VCL source code, take time to find out which messages or events are available and which methods can be overridden.

Putting It All Together

This section will cover putting all the previous techniques into a basic component that you can expand and enhance. This component is not complete, although it could be installed onto C++Builder's Component Palette and used in an application. As a component writer, you should never leave things to chance; the easier your component is to use, the more likely it will be used. The example shown in Listings 4.46 and 4.47 will be a type of Button component that responds like a TSpeedButton and has a bitmap and text. The source code is shown in Listings 4.46 and 4.47, and then we'll look at some of the obvious enhancements that could be made. The source code is also provided in the ExampleButton folder on the CD-ROM that accompanies this book.

Listing 4.46 The TExampleButton Header File, ExampleButton.h
 //-------------------------------------------------------------------------- #ifndef ExampleButtonH  #define ExampleButtonH  //--------------------------------------------------------------------------- #include <SysUtils.hpp>  #include <Controls.hpp>  #include <Classes.hpp>  #include <Forms.hpp>  //-------------------------------------------------------------------------- enum TExButtonState {esUp, esDown, esFlat, esDisabled};  class PACKAGE TExampleButton : public TGraphicControl  {  private:      Graphics::TBitmap *FGlyph;      AnsiString FCaption;      TImageList *FImage;      TExButtonState FState;      bool FMouseInControl;      TNotifyEvent FOnClick;      void __fastcall SetGlyph(Graphics::TBitmap *Value);      void __fastcall SetCaption(AnsiString Value);      void __fastcall BeforeDestruction(void);      void __fastcall SwapColors(TColor &Top, TColor &Bottom);      void __fastcall CalcGlyphLayout(TRect &r);      void __fastcall CalcTextLayout(TRect &r);      MESSAGE void __fastcall CMMouseEnter(TMessage &Msg);      MESSAGE void __fastcall CMMouseLeave(TMessage &Msg);      MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg);  protected:      void __fastcall Paint(void);      void __fastcall MouseDown(TMouseButton Button, TShiftState Shift,                                int X, int Y);      void __fastcall MouseUp(TMouseButton Button, TShiftState Shift,                              int X, int Y);  public:      __fastcall TExampleButton(TComponent* Owner);  __published:      __property AnsiString Caption = {read=FCaption, write=SetCaption};      __property Graphics::TBitmap * Glyph = {read=FGlyph, write=SetGlyph};      __property TNotifyEvent OnClick = {read=FOnClick, write=FOnClick};  BEGIN_MESSAGE_MAP    MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)    MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)    MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)  END_MESSAGE_MAP(TGraphicControl)  };  //-------------------------------------------------------------------------- #endif 
Listing 4.47 The TExampleButton Source File, ExampleButton.cpp
 //-------------------------------------------------------------------------- #include <vcl.h>  #pragma hdrstop  #include "ExampleButton.h"  #pragma package(smart_init)  //-------------------------------------------------------------------------- // ValidCtrCheck is used to assure that the components created do not have  // any pure virtual functions.  //  static inline void ValidCtrCheck(TExampleButton *)  {      new TExampleButton(NULL);  }  //-------------------------------------------------------------------------- __fastcall TExampleButton::TExampleButton(TComponent* Owner)    : TGraphicControl(Owner)  {      SetBounds(0,0,50,50);      ControlStyle = ControlStyle << csReplicatable;      FState = esFlat;  }  //-------------------------------------------------------------------------- namespace Examplebutton  {      void __fastcall PACKAGE Register()      {         TComponentClass classes[1] = {__classid(TExampleButton)};         RegisterComponents("Samples", classes, 0);      }  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::CMMouseEnter(TMessage &Msg)  {      if(Enabled)         {         FState = esUp;         FMouseInControl = true;         Invalidate();         }  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::CMMouseLeave(TMessage &Msg)  {      if(Enabled)         {         FState = esFlat;         FMouseInControl = false;         Invalidate();         }  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::CMEnabledChanged(TMessage &Msg)  {      FState = (Enabled) ? esFlat : esDisabled;      Invalidate();  }  // --------------------------------------------------------------------------- void __fastcall TExampleButton::MouseDown(TMouseButton Button, TShiftState    Shift, int X, int Y)  {      if(Button == mbLeft)         {         if(Enabled && FMouseInControl)             {             FState = esDown;             Invalidate();             }         }  }  // --------------------------------------------------------------------------- void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState    Shift, int X, int Y)  {      if(Button == mbLeft)          {          if(Enabled && FMouseInControl)              {              FState = esUp;              Invalidate();              if(FOnClick)                  FOnClick(this);              }          }  }  // --------------------------------------------------------------------------- void __fastcall TExampleButton::SetGlyph(Graphics::TBitmap * Value)  {      if(Value == NULL)          return;      if(!FGlyph)          FGlyph = new Graphics::TBitmap;      FGlyph->Assign(Value);      Invalidate();  }  // --------------------------------------------------------------------------- void __fastcall TExampleButton::SetCaption(AnsiString Value)  {      FCaption = Value;      Invalidate();  }  // --------------------------------------------------------------------------- void __fastcall TExampleButton::SwapColors(TColor &Top, TColor &Bottom)  {      if(ComponentState.Contains(csDesigning))          {          FState = esUp;          }      Top = (FState == esUp) ? clBtnHighlight : clBtnShadow;      Bottom = (FState == esDown) ? clBtnHighlight : clBtnShadow;  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::BeforeDestruction(void)  {      if(FImage)          delete FImage;      if(FGlyph)          delete FGlyph;  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::Paint(void)  {      TRect cRect, tRect, gRect;      TColor TopColor, BottomColor;      Canvas->Brush->Color = clBtnFace;      Canvas->FillRect(ClientRect);      cRect = ClientRect;      Graphics::TBitmap *bit = new Graphics::TBitmap;      bit->Width = ClientWidth;      bit->Height = ClientHeight;      bit->Canvas->Brush->Color = clBtnFace;      bit->Canvas->FillRect(cRect);      if(FGlyph)          if(!FGlyph->Empty)              {              CalcGlyphLayout(gRect);              bit->Canvas->BrushCopy(gRect, FGlyph,                  Rect(0,0,FGlyph->Width,FGlyph->Height),                    FGlyph->TransparentColor);              }      if(!FCaption.IsEmpty())          {          CalcTextLayout(tRect);          bit->Canvas->TextRect(tRect, tRect.Left,tRect.Top, FCaption);          }      if(FState == esUp  FState == esDown)          {          SwapColors(TopColor, BottomColor);          Frame3D(bit->Canvas, cRect, TopColor, BottomColor, 1);          }      BitBlt(Canvas->Handle, 0, 0, ClientWidth, ClientHeight,        bit->Canvas->Handle, 0, 0, SRCCOPY);      delete bit;  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::CalcGlyphLayout(TRect &r)  {      int TotalHeight=0;      int TextHeight=0;      if(!FCaption.IsEmpty())          TextHeight = Canvas->TextHeight(FCaption);      // the added 5 could be a 'Spacing' property but for simplicity we just      // added the 5.      TotalHeight = FGlyph->Height + TextHeight + 5;      r = Rect((ClientWidth/2)-(FGlyph->Width/2),              ((ClientHeight/2)-(TotalHeight/2)), FGlyph->Width +              (ClientWidth/2)-(FGlyph->Width/2), FGlyph->Height +              ((ClientHeight/2)-(TotalHeight/2)));  }  // -------------------------------------------------------------------------- void __fastcall TExampleButton::CalcTextLayout(TRect &r)  {      int TotalHeight=0;      int TextHeight=0;      int TextWidth=0;      TRect temp;      if(FGlyph)          TotalHeight = FGlyph->Height;      TextHeight = Canvas->TextHeight(FCaption);      TextWidth = Canvas->TextWidth(FCaption);      TotalHeight += TextHeight + 5;      temp.Left = 0;      temp.Top = (ClientHeight/2)-(TotalHeight/2);      temp.Bottom = temp.Top + TotalHeight;      temp.Right = ClientWidth;      r = Rect(((ClientWidth/2) - (TextWidth/2)), temp.Bottom-TextHeight,          ((ClientWidth/2)-(TextWidth/2))+TextWidth,  temp.Bottom);  } 

Here only the OnClick event is published. In a real component you would more than likely publish the OnMouseUp , OnMouseDown , and OnMouseMove events as well. The two properties Caption and Glyph are published, but you should have a Font property to enable users to change the font of the caption.

It would probably be a good idea to catch the CM_FONTCHANGED message so that the positions of the button glyph and caption can be redrawn accordingly . In calculating the position of the image and the text, we use a value of five pixels as the spacing between the two. It would also be a good idea to create a property that enables the user to specify this value.

In Listing 4.47, take a look at the write method for the Glyph property, SetGlyph() . If a NULL pointer is assigned to the Glyph property, the method simply returns without doing anything. This might seem like typical behavior for this type of property, but after you have assigned an image there is no way to get rid of it. In other words, you cannot show only the caption without deleting the component and creating a new one.

The last thing we will look at is the Boolean FMouseInControl FMouseInControl>variable. Because the control is responding to mouse events, it is wise to keep track of it. This variable is used to track whether the mouse cursor is over the control. Without this variable in place, certain member functions would be called inappropriately because the component will still receive mouse events even though the action did not begin over the control. For example, if a user clicked and held the mouse button, and then moved the mouse over the component and released the button, the CMMouseUp() method would be called without the component knowing that the mouse is actually over the control. This in effect would cause the component to redraw itself in its Up state and would not redraw unless you moved the mouse away, and then back again or clicked the button. The FMouseInControl variable prevents this.

In Listing 4.47, the shape of the button is drawn using the Frame3D() method. By including the Buttons.hpp header file in your source file, you can gain access to another method for drawing button shapes, DrawButtonFace() , shown in Listing 4.48.

Listing 4.48 The DrawButtonFace() Method
 TRect DrawButtonFace(TCanvas *Canvas, const TRect Client,    int BevelWidth, TButtonStyle Style, bool IsRounded, bool IsDown,    bool IsFocused); 

The DrawButtonFace() method will draw a button shape the size of Client on the Canvas specified. Some of the parameters are effective according to the value of the others. For example, the BevelWidth and IsRounded parameters seem to have an effect only when Style is bsWin31 . IsFocused has no apparent effect.

The DrawButtonFace() method uses the DrawEdge() API (see the Win32 online help included with C++Builder). This can also be used in your own drawing routines.

Modifying Windowed Components

As stated previously, windowed components are wrappers for the standard Windows controls. These components already know how to draw themselves , so you don't have to worry about that. In modifying windowed controls, you most likely will want to change what the component actually does rather than its look. Fortunately, the VCL provides protected methods in these components that can be overridden to do just that.

In this last example we'll use some of the techniques shown in this chapter to create a more familiar and robust replacement for the TFileListBox component that comes standard with C++Builder. Before we write any code, it's a good idea to get an overview of what we want to accomplish. Remember that we want to make this component as easy to use as possible and relieve the user from the task of writing code that is common when using a component that lists filenames. The following lists the changes we'll make in our component:

  • Display the correct icon for each file.

  • Give the user the ability to launch an application or open a document when the item is double-clicked.

  • Allow the user the option to add a particular item to the list box.

  • Allow an item to be selected when right-clicked.

  • Show a horizontal scrollbar when an item is longer than the width of the list box.

  • Maintain compatibility with TDirectoryListBox .

Now that we know what our component should do, we must decide from which base class to derive it. As stated previously, C++Builder provides custom classes from which to derive new components, which is currently not the case in our component. TDirectoryListBox and TFileListBox are linked together through the FileList property of TDirectoryListBox . This property is declared as a pointer to a TFileListBox , so a component derived from TCustomListBox or TListBox will not be visible to the property. To maintain compatibility with TDirectoryListBox , we will have to derive our component from TFileListBox . Fortunately, the methods it uses to read the filenames are protected, so all we have to do is override them in our new component.

Next, we'll consider the changes we want to make to this component and declare some new properties, methods, and events. First we want to allow the user to launch an application or open a document by double-clicking an item. We can declare a Boolean property that will allow the user to turn this option on or off, as shown in the following code.

 __property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch, default=true}; 

When a user double-clicks the list box, it is sent a WM_LBUTTONDBLCLK message. TCustomListBox conveniently provides a protected method that is called in response to this message. Listing 4.49 shows how we can override the DblClick() method to launch an application or open a document.

Listing 4.49 The DblClick() Method
 void __fastcall TSHFileListBox::DblClick(void)  {    if(FCanLaunch)      {      int ii=0;      // go through the list and find which item is selected      for(ii=0; ii < Items->Count; ii++)        {        if(Selected[ii])          {          AnsiString str = Items->Strings[ii];          ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);          }        }      }    // fire the OnDblClick event    if(FOnDblClick)       FOnDblClick(this);  } 

It the FCanLaunch variable is true , we must first find which item is selected, and then use the ShellExecute() API to launch the application. This method also fires an OnDblClick event, which is declared as shown in the following code.

 private:    TNotifyEvent FOnDblClick;  __published:    __property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick}; 

The OnDblClick event does not really need to provide any information, so we can declare it as a TNotifyEvent variable. This behavior can certainly be changed if the need arises, but for now this will suffice. Now let's tackle the problem of allowing an item to be selected when right-clicked. First, we need to declare a new property as shown in the following code.

 __property bool RightBtnClick = {read=FRightBtnSelect, write=FRightBtnSelect,    default=true}; 

Notice that the property is referencing a member variable, and there is no read or write method. This is because we'll use the member variable in the MouseUp() event to determine whether to select the item. Listing 4.50 shows the code for the MouseUp() method.

Listing 4.50 The MouseUp() Method
 //--------------------------------------------------------------------------- void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState Shift,    int X, int Y)  {    if(!FRightBtnSel)      return;    TPoint ItemPos = Point(X,Y);    // is there an item under the mouse ?    int Index = ItemAtPos(ItemPos, true);    // if not just return    if(Index == -1)      return;    // else select the item    Perform(LB_SETCURSEL,  (WPARAM)Index, 0);  } 

The code in Listing 4.50 is fairly simple. First we check the FRightBtnSel variable to see if we can select the item. Next, we need to convert the mouse coordinates to a TPoint structure. To find which item is under the mouse, we use the ItemAtPos() method of TcustomListBox , which takes the TPoint structure we created and a Boolean value that determines if the return value should be -1 or one more than the last item in the list box if the coordinate contained in the TPoint structure is beyond the last item. Here the parameter is true and, if the return value is -1 , the method simply returns. You could change this value to false and remove the if() statement that checks the return value. Finally, we use the Perform() method to force the control to act as if it has received a window message. The first parameter to the Perform() method is the actual message we want to simulate. LB_SETCURSEL tells the list box that the mouse has changed the selection. The second parameter is the index of the item we want to select; the third parameter is not used, so it is zero.

Next, we want to enable the user the option to add a particular item. TFileListBox has a Mask property that enables you to specify file extensions that can be added to the list box. It is possible to redeclare the Mask property and provide a read and write method that filters the filenames according to the value of the Mask property. You took the easy way out and chose to provide an event that enables the user to apply his own algorithm for filtering the filenames. You could keep this event in place and still cater to the Mask property to provide even more functionality.

First, let's declare our new event.

 typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString    Item, bool &CanAdd); 

As you can see, the event provides three parameters. Sender is the list box, Item is an AnsiString value that contains the filename, and CanAdd is a Boolean value that determines if the item can be added. Notice that the CanAdd parameter is passed by reference so that a user can change this value to false in the event handler to prevent Item from being added to the items in the list box. Before we look at how to get the filenames and add them to the list box, let's look at Listing 4.51 and see how we can use the same icons as Windows Explorer.

Listing 4.51 Getting the System Image List
 SHFILEINFO shfi;  DWORD iHnd;  TImageList *Images;  Images = new TImageList(this);  Images->ShareImages = true;  iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX                                         SHGFI_SHELLICONSIZE  SHGFI_SMALLICON);  if(iHnd != 0)      Images->Handle = iHnd; 

Notice in Listing 4.51 that the ShareImages property of FImages is set to true . This is very important. It informs the image list that it should not destroy its handle when the component is destroyed. The handle of the system image list belongs to the system, and if your component destroys it, Windows will not be able to display any of its icons for menus and shortcuts. This isn't permanent; you would just have to reboot your system so that Windows could get the handle of the system images again.

At this point we can override the ReadFileNames() method of TFileListBox to retrieve the filenames in a slightly different manner. Our version will use the shell to get the filenames using COM interfaces. Because walking an itemid list can look a bit messy and is beyond the scope of this chapter, we will not go into detail. We will create a new method, AddItem() , shown in Listing 4.52. It will retrieve the display name of the file and its icon index in the system image list and fire the OnAddItem event we created previously.

NOTE

An itemid is another name for an item identifier or identifier list. You can find more information about "Item Identifiers and Identifier Lists" in the Win32 help files that ship with C++Builder.


Listing 4.52 The AddItem() Method
 int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)  {    SHFILEINFO shfi;    int Index;    SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL  SHGFI_SYSICONINDEX       SHGFI_SMALLICON  SHGFI_DISPLAYNAME  SHGFI_USEFILEATTRIBUTES);    // fire the OnAddItem event to allow the user the choice to add the    // file name or not    bool FCanAdd = true;    if(FOnAddItem)      FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);    if(FCanAdd)      {      TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);      Index = Items->AddObject(AnsiString(shfi.szDisplayName), (TObject*)ShellInfo);      // return the length of the file name      return Canvas->TextWidth(Items->Strings[Index]);      }    // return zero as the length as the file has not been added    return 0;  } 

The AddItem() method takes an itemid as its only parameter and returns an integer value. In Listing 4.52 we use the SHGetFileInfo() API to retrieve the display name of the file and its icon index. After we have the file's display name, we create a Boolean variable named CanAdd to determine if the item can be added, and then fire the OnAddItem event. We can then check the value of CanAdd and, if it is true , we go ahead and add the new item to the list box. After the item is added, we use the TextWidth() method of TCanvas to get its width in pixels. This value is the return value of the method, if the item was added, or zero if not. You will see the reason for this shortly.

One thing that we haven't discussed yet is the TShellFileListItem class. Because we need to do the actual drawing of the icons and text in the list box, we need some way of keeping track of each item's icon index. For each item that is added to the list box, we create an instance of TShellFileListItem and assign it to the Object property of the list box's Items property. This way we can retrieve it later when we need to draw the item's icon. TShellFileListItem also holds a copy of the item's itemid . This is for possible future enhancements; for example, you could create a descendant of TSHFileListBox and override the MouseUp() method to display the context menu for the file.

One thing to remember about using the Object property in this way is that the memory being used to hold the TShellFileListItem instance must be freed when the item is deleted from the list box. We do this by overriding the DeleteString() method, as shown in Listing 4.55.

As stated previously, the return value from the AddItem() method is the length in pixels of the item that has just been added to the list box. This value is used to determine the longest item and to display a horizontal scrollbar if the longest item is longer than the width of the list box. Take a look at the following code:

 while(Fetched > 0)            {            // add the item to the listbox            int l = AddItem(rgelt);            if(l > hExtent)              hExtent = l;            ppenumIDList->Next(celt, &rgelt, &Fetched);            } 

This is a snippet from the ReadFileNames() method. It loops through a folder's itemid list and retrieves an itemid for each file. The AddItem() method returns the item's length and compares it to the previous item. If it is longer, the variable l is assigned the new length and the process is repeated until no more files remain . At the end of this loop, l holds the length of the longest item. Then, the DoHorizontalScrollBar() method can be called to determine if the horizontal scrollbar needs to be displayed.

The DoHorizontalScrollBars() method, shown in Listing 4.53, takes an integer value as its parameter. This is the length in pixels of an item just added to the list. The value is increased by two pixels for the left margin and, if the ShowGlyphs property is true , 18 pixels more is added to compensate for the width of the image and spacing between the image and text. Finally, the Perform() method is called to set the horizontal extent of the items in the list box, which in effect will show the scrollbar if the value of WPARAM is greater than the width of the control.

Listing 4.53 Adding a Horizontal Scrollbar
 void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)  {      he += 2;      if(ShowGlyphs)          he += 18;      Perform(LB_SETHORIZONTALEXTENT,  he, 0);  } 

Listings 4.54 and 4.55 are the full source code for the TSHFileListBox component, which can be found in the SHFileListBox folder on the CD-ROM that accompanies this book.

Listing 4.54 The TSHFileListBox Header File, SHFileListBox.h
 //-------------------------------------------------------------------------- #ifndef SHFileListBoxH  #define SHFileListBoxH  //-------------------------------------------------------------------------- #include <SysUtils.hpp>  #include <Controls.hpp>  #include <Classes.hpp>  #include <Forms.hpp>  #include <FileCtrl.hpp>  #include <StdCtrls.hpp>  #include "ShlObj.h"  //-------------------------------------------------------------------------- class TShellFileListItem : public TObject  {  private:    LPITEMIDLIST Fpidl;    int FImageIndex;  public:    __fastcall TShellFileListItem(LPITEMIDLIST lpidl, int Index);    __fastcall ~TShellFileListItem(void);    __property LPITEMIDLIST pidl = {read=Fpidl};    __property int ImageIndex = {read=FImageIndex};  };  typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString    Item, bool &CanAdd);  class PACKAGE TSHFileListBox : public TFileListBox  {  private:    TImageList *FImages;    TNotifyEvent FOnDblClick;    bool FCanLaunch;    bool FRightBtnSel;    TAddItemEvent FOnAddItem;    int __fastcall AddItem(LPITEMIDLIST pidl);    void __fastcall GetSysImages(void);  protected:    void __fastcall DblClick(void);    void __fastcall ReadFileNames(void);    void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X,      int Y);    void __fastcall DrawItem(int Index, const TRect &Rect,      TOwnerDrawState State);    void __fastcall DoHorizontalScrollBar(int he);    void __fastcall DeleteString(int Index);  public:    __fastcall TSHFileListBox(TComponent* Owner);    __fastcall ~TSHFileListBox(void);  __published:    __property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch,      default=true};    __property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel,      default=true};    __property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick};    __property TAddItemEvent OnAddItem = {read=FOnAddItem, write=FOnAddItem};  };  #endif 
Listing 4.55 The TSHFileListBox Source File, SHFileListBox.cpp
 //-------------------------------------------------------------------------- #include <vcl.h>  #pragma hdrstop  #include "SHFileListBox.h"  #pragma package(smart_init)  //-------------------------------------------------------------------------- __fastcall TShellFileListItem::TShellFileListItem(LPITEMIDLIST lpidl,    int Index)    : TObject()  {    // store a copy of the file's pidl    Fpidl = CopyPIDL(lpidl);    // and save its icon index    FImageIndex = Index;  }  //-------------------------------------------------------------------------- __fastcall TShellFileListItem::~TShellFileListItem(void)  {    LPMALLOC lpMalloc=NULL;    if(SUCCEEDED(SHGetMalloc(&lpMalloc)))      {      // free the memory associated with the pidl      lpMalloc->Free(Fpidl);      lpMalloc->Release();      }  }  //-------------------------------------------------------------------------- __fastcall TSHFileListBox::TSHFileListBox(TComponent* Owner)    : TFileListBox(Owner)  {    ItemHeight = 18;    ShowGlyphs = true;    FCanLaunch = true;    FRightBtnSel = true;  }  //-------------------------------------------------------------------------- __fastcall TSHFileListBox::~TSHFileListBox(void)  {    // free the images    if(FImages)      delete FImages;    FImages = NULL;  }  //-------------------------------------------------------------------------- void __fastcall TSHFileListBox::DeleteString(int Index)  {    // This method is called in response to the LB_DELETESTRING messeage    // First delete the TShellFileListItem pointed to by the string's    // Object property    TShellFileListItem *ShellItem = reinterpret_cast<TShellFileListItem*>      (Items->Objects[Index]);    delete ShellItem;    ShellItem = NULL;    // now delete the item    Items->Delete(Index);  }  //-------------------------------------------------------------------------- namespace Shfilelistbox  {    void __fastcall PACKAGE Register()    {       TComponentClass classes[1] = {__classid(TSHFileListBox)};       RegisterComponents("Samples", classes, 0);    }  }  //-------------------------------------------------------------------------- void __fastcall TSHFileListBox::ReadFileNames(void)  {    LPMALLOC g_pMalloc;    LPSHELLFOLDER pisf;    LPSHELLFOLDER sfChild;    LPITEMIDLIST pidlDirectory;    LPITEMIDLIST rgelt;    LPENUMIDLIST ppenumIDList;    int hExtent;    try      {      try        {        if(HandleAllocated())          {          GetSysImages();          // prohibit screen updates          Items->BeginUpdate();          // delete the items already in the list          Items->Clear();          // get the shell's global allocator          if(SHGetMalloc(&g_pMalloc) != NOERROR)            {            return;            }          // get the desktop's IShellFolder interface          if(SHGetDesktopFolder(&pisf) != NOERROR)            {            return;            }          // convert folder string to WideChar          WideChar oleStr[MAX_PATH];          FDirectory.WideChar(oleStr, MAX_PATH);          unsigned long pchEaten;          unsigned long pdwAttributes;          // get pidl of current folder          pisf->ParseDisplayName(Handle, 0, oleStr, &pchEaten,                  &pidlDirectory, &pdwAttributes);          // get an IShellFolder interface for the current folder          if(pisf->BindToObject(pidlDirectory,NULL,              IID_IShellFolder, (void**)&sfChild) != NOERROR)            {            return;            }          // enumerate the objects withing the folder          sfChild->EnumObjects(Handle, SHCONTF_NONFOLDERS             SHCONTF_INCLUDEHIDDEN, &ppenumIDList);          // walk through the enumlist          ULONG celt = 1;          ULONG Fetched = 0;          ppenumIDList->Next(celt, &rgelt, &Fetched);          hExtent = 0;          while(Fetched > 0)            {            // add the item to the listbox            int l = AddItem(rgelt);            if(l > hExtent)              hExtent = l;            ppenumIDList->Next(celt, &rgelt, &Fetched);            }          }        }      catch(Exception &E)        {        throw(E); // re-throw any exceptions        }      }    __finally      { // make sure we do this reguardless      g_pMalloc->Free(rgelt);      g_pMalloc->Free(ppenumIDList);      g_pMalloc->Free(pidlDirectory);      pisf->Release();      sfChild->Release();      g_pMalloc->Release();      Items->EndUpdate();      }    // Show the horizontal scrollbar if necessary    DoHorizontalScrollBar(hExtent);  }  // -------------------------------------------------------------------------- void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)  {    // add a little space for the margins    he += 2;    // if we're showing the images make room for it plus a bit more    // for the space between the image and the text    if(ShowGlyphs)      he += 18;    Perform(LB_SETHORIZONTALEXTENT, he, 0);  }  // -------------------------------------------------------------------------- void __fastcall TSHFileListBox::GetSysImages(void)  {    SHFILEINFO shfi;    DWORD iHnd;    if(!FImages)      {      FImages = new TImageList(this);      FImages->ShareImages = true;      FImages->Height = 16;      FImages->Width = 16;      iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX         SHGFI_SHELLICONSIZE  SHGFI_SMALLICON);      if(iHnd != 0)        FImages->Handle = iHnd;      }  }  // -------------------------------------------------------------------------- int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)  {    SHFILEINFO shfi;    int Index;    SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL       SHGFI_SYSICONINDEX       SHGFI_SMALLICON  SHGFI_DISPLAYNAME  SHGFI_USEFILEATTRIBUTES);    // fire the OnAddItem event to allow the user the choice to add the    // file name or not    bool FCanAdd = true;    if(FOnAddItem)      FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);    if(FCanAdd)      {      TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);      Index = Items->AddObject(AnsiString(shfi.szDisplayName),        (TObject*)ShellInfo);      // return the length of the file name      return Canvas->TextWidth(Items->Strings[Index]);      }    // return zero as the length as the file has not been added    return 0;  }  // -------------------------------------------------------------------------- void __fastcall TSHFileListBox::DrawItem(int Index, const TRect &Rect,    TOwnerDrawState State)  {    int Offset;    Canvas->FillRect(Rect);    Offset = 2;    if(ShowGlyphs)      {      TShellFileListItem *ShellItem = reinterpret_cast<TShellFileListItem*>        (Items->Objects[Index]);      // draw the file's icon in the listbox      FImages->Draw(Canvas, Rect.Left+2, Rect.Top+2, ShellItem->ImageIndex,        true);      Offset += 18;      }    int Texty = Canvas->TextHeight(Items->Strings[Index]);    Texty = ((ItemHeight - Texty) / 2) + 1;    // now draw the text    Canvas->TextOut(Rect.Left + Offset, Rect.Top + Texty,  Items->Strings[Index]);  }  //-------------------------------------------------------------------------- void __fastcall TSHFileListBox::DblClick(void)  {    if(FCanLaunch)      {      int ii=0;      // go through the list and find which item is selected      for(ii=0; ii < Items->Count; ii++)        {        if(Selected[ii])          {          AnsiString str = Items->Strings[ii];          ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);          }        }      }    // fire the OnDblClick event    if(FOnDblClick)      FOnDblClick(this);  }  //-------------------------------------------------------------------------- void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState Shift,    int X, int Y)  {    if(!FRightBtnSel)      return;    TPoint ItemPos = Point(X,Y);    // is there an item under the mouse ?    int Index = ItemAtPos(ItemPos, true);    // if not just return    if(Index == -1)      return;    // else select the item    Perform(LB_SETCURSEL, (WPARAM)Index, 0);  }  //-------------------------------------------------------------------------- // ValidCtrCheck is used to assure that the components created do not have  // any pure virtual functions.  //  static inline void ValidCtrCheck(TSHFileListBox *)  {    new TSHFileListBox (NULL);  } 

Creating Custom Data-Aware Components

Just as with any other custom component, it is important to decide from the start which ancestor will be used for the creation of a data-aware component. In this section, we are going to look at extending the TMaskEdit edit component so that it will read data from a datasource and display it in the masked format provided. This type of control is known as a data-browsing control . We will then extend this control further to make it a data-aware control, meaning that changes to the field or database will be reflected in both directions.

Making the Control Read-Only

The control we are going to create already has ReadOnly , a read-only property, so we don't have to create it. If your component doesn't, create the property as you would for any other component.

If our component did not already have the ReadOnly property, we would create it as shown in Listing 4.56 (note that this code is not required for this component).

Listing 4.56 Creating a Read-Only Property
 class PACKAGE TDBMaskEdit : public TMaskEdit  {  private:      bool FReadOnly;  protected:  public:      __fastcall TDBMaskEdit(TComponent* Owner);  __published:      __property ReadOnly = {read = FReadOnly, write = FReadOnly,        default = true};  }; 

In the constructor we would set the default value of the property.

 __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)    : TMaskEdit(Owner)  {      FReadOnly = true;  } 

Finally, we need to ensure that the component acts as a read-only control. You need to override the method normally associated with the user accessing the control. If we were creating a data-aware grid, it would be the SelectCell() method in which you would check the value of the ReadOnly property and act accordingly. If the value of ReadOnly is false , you call the inherited method, otherwise, just return.

If the TMaskEdit control had a SelectEdit() method, the code would look like this:

 bool __fastcall TDBMaskEdit::SelectEdit(void)  {      if(FReadOnly)         return(false);      else         return(TMaskEdit::SelectEdit());  } 

In this case, we don't have to worry about the ReadOnly property. TMaskEdit already has one.

Establishing the Link

For our control to become data aware, we need to provide it the data link required to communicate with a data member of a database. This data link class is called TFieldDataLink .

A data-aware control owns its data link class. It is the control's responsibility to create, initialize, and destroy the data link.

Establishing the link requires three steps:

  1. Declare the data link class as a member of the control

  2. Declare the read and write access properties as appropriate

  3. Initialize the data link

Declare the Data Link

The data link is a class of type TFieldDataLink and requires DBCTRLS.HPP to be included in the header file.

 #include <DBCtrls.hpp>  class PACKAGE TDBMaskEdit : public TMaskEdit  {  private:      TFieldDataLink *FDataLink;    ...  }; 

Our data-aware component now requires DataSource and DataField properties (just like all other data-aware controls). These properties use pass-through methods to access properties of the data link class. This enables the control and its data link to share the same datasource and field.

Declare read and write Access

The access you allow your control is governed by the declaration of the properties themselves. We are going to give our component full access. It has a ReadOnly property that will automatically take care of the read-only option because the user will be unable to edit the control. Note that this will not stop the developer from writing code to write directly to the linked field of the database via this control. If you require read-only access, simply leave out the write option.

The code in Listings 4.57 and 4.58 shows the declaration of the properties and their corresponding read and write implementation methods.

Listing 4.57 The TDBMaskEdit Class Declaration from the Header File
 class PACKAGE TDBMaskEdit : public TMaskEdit  {  private:      ...      AnsiString __fastcall GetDataField(void);      TDataSource* __fastcall GetDataSource(void);      void __fastcall SetDataField(AnsiString pDataField);      void __fastcall SetDataSource(TDataSource *pDataSource);      ...  __published:      __property AnsiString DataField = {read = GetDataField,        write = SetDataField, nodefault};      __property TDataSource *DataSource = {read = GetDataSource,        write = SetDataSource,  nodefault};  }; 
Listing 4.58 The TDBMaskEdit Methods from the Source File
 AnsiString __fastcall TDBMaskEdit::GetDataField(void)  {      return(FDataLink->FieldName);  }  TDataSource * __fastcall TDBMaskEdit::GetDataSource(void)  {      return(FDataLink->DataSource);  }  void __fastcall TDBMaskEdit::SetDataField(AnsiString pDataField)  {      FDataLink->FieldName = pDataField;  }  void __fastcall TDBMaskEdit::SetDataSource(TDataSource *pDataSource)  {      if(pDataSource != NULL)          pDataSource->FreeNotification(this);      FDataLink->DataSource = pDataSource;  } 

The only code here that requires additional explanation is the FreeNotification() method of pDataSource . C++Builder maintains an internal list of objects so that all other objects can be notified when the object is about to be destroyed. The FreeNotification() method is called automatically for components on the same form, but in this case there is a chance that a component on another form (such as a data module) has references to it. As a result, we need to call FreeNotification() so that the object can be added to the internal list for all other forms.

Initialize the Data Link

You might think everything that needs to be done has been done. If you attempt to compile this component and add it to a form, you will find access violations reported in the Object Inspector for the DataField and DataSource properties. The reason is that the internal FieldDataLink object has not been instantiated .

Add the following declaration to the public section of the class's header file:

 __fastcall ~TDBMaskEdit(void); 

Add the following code to the component's constructor and destructor:

 __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)    : TMaskEdit(Owner)  {      FDataLink = new TFieldDataLink();      FDataLink->Control = this;  }  __fastcall TDBMaskEdit::~TDBMaskEdit(void)  {      if(FDataLink)          {          FDataLink->Control = 0;          FDataLink->OnUpdateData = 0;          delete FDataLink;      }  } 

The Control property of FDataLink is of type TComponent . This property must be set to the component that uses the TFieldDataLink object to manage its link to a TField object. We need to set the Control property to this to indicate that this component is responsible for the link.

Accessing the TObject is achieved by adding a read-only property. Add the property to the public section of the class definition.

 __property TField *Field = {read = GetField}; 

Add the GetField declaration to the private section:

 TField * __fastcall GetField(void); 

Add the following code to the source file:

 TField * __fastcall TDBMaskEdit::GetField(void) 
 {      return(FDataLink->Field);  } 
Using the OnDataChange Event

So far we have created a component that can link to a datasource, but doesn't yet respond to data changes. We are now going to add code that enables the control to respond to changes in the field, such as moving to a new record.

Data link classes have an OnDataChange event that is called when the datasource indicates a change to the data. To give our component the capability to respond to these changes, we add a method and assign it to the OnDataChange event.

NOTE

TDataLink is a helper class used by data-aware objects. Look in the online help files that ship with C++Builder for a listing of its properties, methods, and events.


The OnDataChange event is of type TNotifyEvent , so we need to add our method with the same prototype. Add the following line of code to the private section of the component header.

 class PACKAGE TDBMaskEdit : public TMaskEdit  {  private:      //       void __fastcall DataChange(TObject *Sender);  } 

We need to assign the DataChange() method to the OnDataChange event in the constructor. We also remove this assignment in the component destructor.

 __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)    : TMaskEdit(Owner)  {      FDataLink = new TFieldDataLink();      FDataLink->Control = this;      FDataLink->OnDataChange = DataChange;  }  __fastcall TDBMaskEdit::~TDBMaskEdit(void)  {      if(FDataLink)          {          FDataLink->Control = 0;          FDataLink->OnUpdateData = 0;          FDataLink->OnDataChange = 0;          delete FDataLink;          }  } 

Finally, define the DataChange() method as shown in the following code:

 void __fastcall TDBMaskEdit::DataChange(TObject *Sender)  {      if(!FDataLink->Field)          {          if(ComponentState.Contains(csDesigning))              Text = Name;          else              Text = "";          }      else          Text = FDataLink->Field->AsString;  } 

The DataChange() method first checks to see if the data link is pointing to a datasource (and field). If there is no valid pointer, the Text property (a member of the inherited component) is set to an empty string (at runtime) or the control name (at design time). If a valid field is set, the Text property is set to the value of the field's content via the AsString property of the TField object.

You now have a data-browsing control, so-called because it is capable only of displaying data changes in a datasource. It's now time to turn this component into a data-editing control.

Changing to a Data-Editing Control

Turning a data-browsing control into a data-editing control requires additional code to respond to key and mouse events. This enables any changes made to the control to be reflected in the underlying field of the linked database.

The ReadOnly Property

When a user places a data-editing control into his project, he expects the control not to be read-only. The default value for the ReadOnly property of TMaskEdit (the inherited class) is false , so we have nothing further to do. If you create a component that has a custom ReadOnly property added, be sure to set the default value to false .

Keyboard and Mouse Events

If you refer to the controls.hpp file, you will find protected methods of TMaskEdit called KeyDown() and MouseDown() . These methods respond to the corresponding window messages ( WM_KEYDOWN , WM_LBUTTONDOWN , WM_MBUTTONDOWN , and WM_RBUTTONDOWN ) and call the appropriate event if the user defines one.

To override these methods, add the KeyDown() and MouseDown() methods to the TDBMaskEdit class. Take the declarations from the controls.hpp file.

 virtual void __fastcall MouseDown(TMouseButton, TShiftState Shift, int X,    int Y);  virtual void __fastcall KeyDown(unsigned short &Key, TShiftState Shift); 

Refer to the controls.hpp file in your C++ Builder installation (or the help file) to see the original declaration.

Next we add the source code, shown in Listing 4.59.

Listing 4.59 The MouseDown() and KeyDown() Methods
 void __fastcall TDBMaskEdit::MouseDown(TMouseButton Button, TShiftState Shift,    int X, int Y)  {      if(!ReadOnly && FDataLink->Edit())          TMaskEdit::MouseDown(Button, Shift, X, Y);      else          {          if(OnMouseDown)              OnMouseDown(this, Button, Shift, X , Y);          }  }  void __fastcall TDBMaskEdit::KeyDown(unsigned short &Key, TShiftState Shift)  {      Set<unsigned short, VK_PRIOR, VK_DOWN> Keys;      Keys = Keys << VK_PRIOR << VK_NEXT << VK_END << VK_HOME << VK_LEFT        << VK_UP << VK_RIGHT << VK_DOWN;      if(!ReadOnly && (Keys.Contains(Key)) && FDataLink->Edit())          TMaskEdit::KeyDown(Key, Shift);      else          {          if(OnKeyDown)              OnKeyDown(this,  Key, Shift);          }  } 

In both cases, we check to make sure the component is not read-only and the FieldDataLink is in edit mode. The KeyDown() method also checks for any cursor control keys (defined in winuser.h ). If all checks pass, the field can be edited, so the inherited method is called. This method will automatically call the associated user event if one is defined. If the field cannot be edited, the user event is executed (if one exists).

Working Toward a Dataset Update

If the user modifies the contents of the data-aware control, the change must be reflected in the field. Similarly, if the field value is altered, the data-aware control will require a corresponding update.

The TDBMaskEdit control already has a DataChange() method that is called by the TFieldDataLink OnDataChange event. This method reflects the change of the field value in the TDBMaskEdit control. This takes care of the first scenario.

Now we need to update the field value when the user modifies the contents of the control. The TFieldDataLink class has an OnUpdateData event where the data-aware control can write any pending edits to the record in the dataset. We can now create an UpdateData() method for TDBMaskEdit and assign this method to the OnUpdateData event of the TFieldDataLink class.

Add the declaration for our UpdateData() method to the TDBMaskEdit control, as shown in the following code:

 void __fastcall UpdateData(TObject *Sender); 

Assign this method to the TFieldDataLink OnUpdateData event in the constructor:

 __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)    : TMaskEdit(Owner)  {      FDataLink = new TFieldDataLink();      FDataLink->Control = this;      FDataLink->OnUpdateData = UpdateData;      FDataLink->OnDataChange = DataChange;  } 

Set the field value to the current contents of the TDBMaskEdit control:

 void __fastcall TDBMaskEdit::UpdateData(TObject *Sender)  {      if(FDataLink->CanModify)          FDataLink->Field->AsString = Text;  } 

The TDBMaskEdit control is a descendant of TMaskEdit , which happens to be a descendant of the TCustomEdit class. This class has a protected Change() method that is triggered by Windows events. This method then triggers the OnChange event.

We are going to override the Change() method so that it updates the dataset before calling the inherited method. In the protected section of the TDBMaskEdit class, add the following method:

 DYNAMIC void __fastcall Change(void); 

Add the Change() method in Listing 4.60 to the source code.

Listing 4.60 The Change() Method
 void __fastcall TDBMaskEdit::Change(void)  {      if(FDataLink)          {          // we need to see if the datasource is in edit mode          // if not then we need to save the current value because          // placing the datasource into edit mode will change the          // current value to that already present in the table          AnsiString ChangedValue = Text;          // get cursor position too          int CursorPosition = SelStart;          // need to be in edit mode          if(FDataLink->CanModify && FDataLink->Edit())              {              Text = ChangedValue;      // just in case we were not in edit mode              SelStart = CursorPosition;              FDataLink->Modified();    // posting a change (the datasource                                        // is not put back into edit mode)              }          }      TMaskEdit::Change ();  } 

This change notifies the TFieldDataLink class that modifications have been made and finishes up by calling the inherited Change() method.

The final step is to provide for when focus is moved away from the control. The TWinControl class responds to the CM_EXIT message by generating an OnExit event.

We can also respond to this message as a method of updating the record of the linked dataset. Creating a message map in the TDBMaskEdit class does this. Add the following code to the private section:

 void __fastcall CMExit(TWMNoParams Message);  BEGIN_MESSAGE_MAP    MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit)  END_MESSAGE_MAP(TMaskEdit) 

This message map indicates that the CMExit() method will be called in response to a CM_EXIT message with the relevant information passed in the TWMNoParams structure.

The CMExit() method is added to the source file.

 void __fastcall TDBMaskEdit::CMExit(void)  {      try          {          ValidateEdit();          if(FDataLink && FDataLink->CanModify)              FDataLink->UpdateRecord();          }      catch(...)          {          SetFocus();          throw;          }  } 

This attempts to validate the contents of the field against the defined mask. If the datasource can be modified, the record is updated in the dataset. If an exception is raised, the cursor is positioned back in the control that caused the problem, and the exception is raised again so the application can handle it.

Adding a Final Message

C++Builder has a component called TDBCtrlGrid . This control displays records from a datasource in a free-form layout. When this component updates its datasource, it sends out the message CM_GETDATALINK . If you perform a search for this in the C++Builder header files, you'll find a message map defined in all of the database controls. Following this with the corresponding .pas file, you will find message handlers such as the following:

 procedure TDBEdit.CMGetDataLink(var Message: TMessage);  begin    Message.Result := Integer(FDataLink);  end; 

We can add this support to our component by adding the message map, declaring the method, and implementing the message handler.

In the private section

 void __fastcall CMGetDataLink(TMessage Message); 

In the public section, modify the message map to look like the following:

 BEGIN_MESSAGE_MAP    MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit)    MESSAGE_HANDLER(CM_GETDATALINK, TMessage, CMGetDataLink)  END_MESSAGE_MAP(TMaskEdit) 

Finally, implement the method in the source file:

 void __fastcall TDBMaskEdit::CMGetDataLink(TMessage Message)  {      Message.Result = (int)FDataLink;  } 

And that's it. We now have a complete data-aware control that behaves just like any other data control.

Registering Components

Registering components is a straightforward, multistage procedure. The first stage is simple. You must ensure that any component you want to install onto the Component Palette does not contain any pure virtual (or pure DYNAMIC ) functions ”in other words, functions of the following form:

 virtual  ReturnType  __fastcall  FunctionName  (  ParameterList  ) = 0; 

Note that the __fastcall keyword is not a requirement of pure virtual functions, but it will be present in component member functions. This is why it is shown.

You can check for pure virtual functions manually by examining the class definition for the component, or you can call the function ValidCtrCheck() , passing a pointer to your component as an argument. The ValidCtrCheck() function is placed anywhere in the implementation file. For a component called TcustomComponent , it is of the form

 static inline void ValidCtrCheck(TCustomComponent *)  {      new TCustomComponent(NULL);  } 

All this function does is try to create an instance of TCustomComponent . Because you cannot create an instance of a class with a pure virtual function, the compiler will give the following compilation errors:

 E2352 Cannot create instance of abstract class 'TCustomComponent'  E2353 Class 'TCustomComponent' is abstract because of 'function' 

The second error will identify the pure virtual function. Both errors refer to this line:

 new TCustomComponent(NULL); 

Using this function is often not necessary because it is not likely you will create a pure virtual function by accident . However, when you use the IDE to create a new component, this function is automatically added to the implementation file. Then, you might as well leave it there just in case.

After you have determined that your component is not an abstract base class and that it can be instantiated from, you can now write the code to perform the actual registration. To do this, you must write a Register() function. The Register() function must be enclosed in a namespace that is the same as the name of the file in which it is contained. There is one proviso that must be met. The first letter of the namespace must be in uppercase, and the remaining letters must be in lowercase. Hence, the Register() function must appear in your code in the following format:

 namespace Thenameofthefilethisisin  {      void __fastcall PACKAGE Register()      {          // Registration code goes here      }  } 

You must not forget the PACKAGE macro in front of the Register() function. Now that the Register() function is in place, it requires only that the component (or components) that we want to register is registered. To do this, use the RegisterComponents() function. This is declared in $(BCB)\Include\Vcl\Classes.hpp as

 extern PACKAGE void __fastcall RegisterComponents(const AnsiString Page,                                          TMetaClass* const * ComponentClasses,                                          const int ComponentClasses_Size); 

RegisterComponents() expects two things to be passed to it: an AnsiString representing the name of the palette page onto which the component is to be installed, and an open array of TMetaClass pointers to the components to be installed. If the AnsiString value for Page does not match one of the palette pages already present in the Component Palette, a new page is created with the name of the AnsiString passed. The value of this argument can be obtained from a string resource if required, allowing different strings to be used for different locales.

The TMetaClass* open array requires more thought. There are essentially two ways of doing this: Use the OPENARRAY macro or create the array by hand. Let's look at an example that illustrates both approaches.

Consider that we want to register three components: TCustomComponent1 , TCustomComponent2 , and TCustomComponent3 . We want to register these onto a new palette page, MyCustomComponents . First, we must obtain the TMetaClass* for each of the three components. We do this by using the __classid operator, for example:

 __classid(TCustomComponent1) 

Using the OPENARRAY macro, we can write the RegisterComponents() function as follows:

 RegisterComponents("MyCustomComponents",                     OPENARRAY(TMetaClass*,                                (__classid(TCustomComponent1),                                  __classid(TCustomComponent2),                                  __classid(TCustomComponent3)))); 

We could use TComponentClass instead of TMetaClass* because it is a typedef for TMetaClass* , declared in $(BCB)\Include\Vcl\Classes.hpp as

 typedef TMetaClass* TComponentClass; 

Note that you are restricted to registering a maximum of 19 arguments (components) in any single RegisterComponents call because limitations of the OPENARRAY macro. Normally this is not a problem.

The other approach is to declare and initialize an array of TMetaClass* (or TComponentClass ) by hand:

 TMetaClass Components[3] = { __classid(TCustomComponent1),                               __classid(TCustomComponent2),                               __classid(TCustomComponent3) }; 

We then pass this to the RegisterComponents() function as before, but this time we must also pass the value of the last valid index for the array, in this case 2 :

 RegisterComponents("MyCustomComponents", Components, 2); 

The final function call is simpler, but there is a greater chance of error in passing an incorrect value for the last parameter.

We can now see what a complete Register() function looks like:

 namespace Thenameofthefilethisisin  {      void __fastcall PACKAGE Register()      {          RegisterComponents("MyCustomComponents",                             OPENARRAY(TMetaClass*,                                        (__classid(TCustomComponent1),                                          __classid(TCustomComponent2),                                          __classid(TCustomComponent3))));      }  } 

Remember that you can have as many RegisterComponents() functions in the Register() function as required. You can also include other registrations such as those for property and component editors. This is the subject of the next chapter. You can place the component registration in the implementation file of the component, but typically the registration code should be isolated from the component implementation.


     
Top


C++ Builder Developers Guide
C++Builder 5 Developers Guide
ISBN: 0672319721
EAN: 2147483647
Year: 2002
Pages: 253

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net