Overview
This chapter and the next two cover building components. Component
development is one of the most important technologies in contemporary
programming, and no C++
environment on the market makes them easier to use or to create than BCB.
You build three types of components in this chapter:
- Descendants that change default settings in existing components.
- Descendants that add features to existing components.
- Tools built on top of abstract component base classes such as
TWinControl, TCustomControl, TComponent, and
TGraphicControl. You make descendants from these classes if you want to
build your own components from the bottom up.
This chapter presents material on building visual components, Chapter 23
focuses on building unique components that are polymorphic, and Chapter 24
explores building nonvisual components. Nonvisual components descend directly
from TComponent and appear on the form only at design time.
More specifically, the components built in this chapter fall into two
categories:
- The first group is a set of TEdit, TLabel, and
TPanel descendants that show how to change default colors, captions, and
fonts. This section of the chapter also covers building components that
consist of several different child components; that is, this section shows how
to group components together to form new components. The specific example
included with this book shows a panel that comes with two radio buttons.
- The second tool is a clock component that can be dropped on a form, and
stopped and started at will.
In Chapter 24, you will see a nonvisual control that knows how to iterate
through subdirectories. You can use it to build programs that search for files,
delete all files with a certain extension, and so on.
Besides components, this chapter also briefly covers two related topics:
Property editors are used to edit the properties of components. The classic
examples are the common dialogs that pop up when you edit the Color or
Font properties that belong to most visible components. The drop-down
lists and string-editing capabilities found in the Object Inspector are also
property editors.
- Component editors are associated not with a single property, but with an
entire component. An example is the Fields Editor used with the TTable
and TQuery components.
The property editors and component editors are related to a broader topic
called the Tools API. The Tools API consists of a series of interfaces to the
BCB IDE that allows you to build experts, interfaces to version control systems,
and similar utilities. The API for property editors and component editors and
the Tools API are defined in files with names that end in INTF, which
stands for "Interface." For example, the TPropertyEditor class is found
in DSGNINTF.HPP.
Components, properties, and events, along with their associated component
editors and property editors are perhaps the most important topics in BCB
programming. In many ways, this chapter and the next two are the keystones
around which this entire book has been designed.
The source for all the components created in this chapter is found in the
Chap22
and Utils directories on the CD that accompanies this book. The
source on the CD compiles correctly, and all of these components work as
described in this chapter. Because of the default behavior of the C++ linker,
you might have to run a program called BuildObjs before installing some of these
components. This program is in the Utils directory. A description of
this program is in the readme file on the CD. If you have any trouble with the
code in this chapter, turn to the
readme!
Component Theory
BCB components have three outstanding strengths:
- They are native components, built in BCB's language, without recourse to a
complicated API such as OLE. You therefore can write, debug, and test your
components using the same rules you use in standard BCB programs. You don't
need to learn a whole new programming model, as you do when you write ActiveX
controls. In short, writing a BCB component is about 10 times easier than
writing an OCX or VBX component.
- They are fully object-oriented, which means you can easily change or
enhance existing components by creating descendant objects.
- They are small, fast, and light, and can be linked directly into your
executables. Native BCB components are orders of magnitude smaller than most
ActiveX controls, and they link directly into your programs. In particular, a
native BCB component is just a particular kind of object, so it links into
your programs naturally, just as any other object would, without any
handwaving.
Few people would claim that VBXs aren't groundbreaking, that ActiveX controls
aren't going to be very important, or that OLE2 is not an enormously promising
architecture. However, BCB components are relatively easy to create and come in
a light, easy-to-use package. You can create BCB components that do nearly
anything, from serial communications to database links to multimedia. These
capabilities give BCB a big advantage over other tools that force you to use
large, complex, and unwieldy component models.
NOTE: Most publicly available
components cost in the range of $50 to $150. Many of these tools encapsulate
functionality that might cost tens of thousands of dollars to produce
in-house. For example, a good communication library might take a year to
build. However, if a company can sell the library in volume, it can afford to
charge $100 or $200 for the same product. That's a real bargain. And most of
these tools are easy to use. Building components is a great way for relatively
small third-party companies to make money, and buying components is a great
way to save time on big projects. These ground-breaking tools are changing
everything about the way programs are constructed.
BCB components are flexible tools easily built by anyone who knows OOP and the
BCB language. In this package, you have explanations detailing all the
prerequisite knowledge that component builders need, from a description of BCB
itself, through a description of its language, and on to an overview of its
implementation of OOP. From this foundation, you can easily begin building your
own components.
Creating Descendants of an Existing Component
In this section, you will see how to create a series of custom TEdit,
TPanel, and TLabel controls. The changes made to the standard
TEdit and TLabel components involve tweaking their colors, as
well as their fonts' colors, names, sizes, and styles. The goal is to show how
to create a suite of custom controls that you can place on the Component Palette
and use for special effects, or to define the look and feel of a certain set of
applications belonging to a particular department or company.
With projects like this, starting with one simple example is best, and then
you can move on to more complex components. Listings 22.1 through 22.3 contain a
sample component and a program that allows you to test the new component before
you place it on the Component Palette. The component is stored in a module
called Unleash1. The Unleash1 source code is the first version
of a unit that will be expanded later in the chapter. Scan through it and check
out its basic structure. Once it is clear to you how to create and test the
component, I will briefly discuss how to place it on the Component Palette.
Listing 22.1. The header file for a simple component
descending from TEdit.
///////////////////////////////////////
// Unleash1.h
// Simple example of creating a component
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef Unleash1H
#define Unleash1H
//----------------------------------
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\StdCtrls.hpp>
//----------------------------------
class TSmallEdit : public TEdit
{
private:
protected:
public:
virtual __fastcall TSmallEdit(TComponent* Owner);
__published:
};
//----------------------------------
#endif
Listing 22.2. The code for a simple component
descending from TEdit.
///////////////////////////////////////
// Unleash1.cpp
// Simple example of creating a component
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Unleash1.h"
//----------------------------------
static inline TSmallEdit *ValidCtrCheck()
{
return new TSmallEdit(NULL);
}
//----------------------------------
__fastcall TSmallEdit::TSmallEdit(TComponent* Owner)
: TEdit(Owner)
{
Color = clBlue;
Font->Color = clYellow;
Font->Name = "Times New Roman";
Font->Size = 12;
Font->Style = TFontStyles() << fsBold << fsItalic;
}
//----------------------------------
namespace Unleash1
{
void __fastcall Register()
{
TComponentClass classes[1] = {__classid(TSmallEdit)};
RegisterComponents("Unleash", classes, 0);
}
}
//----------------------------------
Listing 22.3. The main form for the TestUnleash1
program serves as a test bed for the Unleash1 unit.
///////////////////////////////////////
// Main.cpp
// Simple example of creating a component
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "Unleash1.h"
#pragma link "unleash1"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TSmallEdit *MyEdit = new TSmallEdit(this);
MyEdit->Parent = this;
MyEdit->Show();
}
This program is designed to test the TSmallEdit control before you add
it to the Component Palette. The test program has a single button that responds
to clicks. If you click the program's button, the new component appears at the
top left of the form, as shown in Figure 22.1. Once again, the goal of the
program is to make sure the new edit control is meeting your expectations before
trying to compile it as a component.
Figure 22.1.At runtime, the TestUnleash1 main form features a
TButton and a custom Edit control called TSmallEdit.
You can easily create this unit, test it, and compile it as a component
that's merged in with the rest of the tools on the Component Palette. To start
creating the component, choose File | New and select Component from the first
page of the Object Repository. The dialog shown in Figure 22.2 then appears.
Figure 22.2.The Component Wizard dialog.
The Component Wizard is a simple code generator, of the type that any reader
of this book who has made it this far should be able to write in an hour or two.
It simply asks you for the name of the component you want to create and to then
select its parent from a drop-down list. After you define the type of tool you
want to create, you can select the page in the Component Palette where you want
it to reside. You should fill in the blanks with the following information:
Class Name: TSmallEdit
Ancestor type: TEdit
Palette Page: Unleash
For your efforts, the Component Expert churns out the code in Listings 22.4
and 22.5, in which everything is boilerplate except for the first line of the
class declaration, the constructor, and the parameters passed to the
RegisterComponents method.
Listing 22.4. The header produced by the standard
boilerplate output of the Component Expert.
//----------------------------------
#ifndef Unleash1H
#define Unleash1H
//----------------------------------
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\StdCtrls.hpp>
class TSmallEdit : public TEdit
{
private:
protected:
public:
virtual __fastcall TSmallEdit(TComponent* Owner);
__published:
};
#endif
Listing 22.5. The implementation of TSmallEdit as
produced by the standard boilerplate output of the Component Expert.
//----------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
#include "Unleash1.h"
//----------------------------------
static inline TSmallEdit *ValidCtrCheck()
{
return new TSmallEdit(NULL);
}
//----------------------------------
__fastcall TSmallEdit::TSmallEdit(TComponent* Owner)
: TEdit(Owner)
{
}
//----------------------------------
namespace Unleash1
{
void __fastcall Register()
{
TComponentClass classes[1] = {__classid(TSmallEdit)};
RegisterComponents("Unleash", classes, 0);
}
}
//----------------------------------
The Component Expert starts by giving you #include directives designed
to cover most of the bases you are likely to touch in a standard component:
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\StdCtrls.hpp>
The next step is to give you a basic class declaration, in which the name and
parent are filled in with the choices you specified in the Component Expert
dialog. All this business about the scoping directives is just for your
convenience, and you can delete any portion of it that you don't think you'll
need.
class TSmallEdit : public TEdit
{
private:
// Private declarations
protected:
// Protected declarations
public:
// Public declarations
virtual __fastcall TSmallEdit(TComponent* Owner);
__published:
// Published declarations
};
Before you can place a component on the Component Palette, you first must
register it with the system:
void __fastcall Register()
{
TComponentClass classes[1] = {__classid(TSmallEdit)};
RegisterComponents("Unleash", classes, 0);
}
Registering a class makes it known to the BCB Component Palette when the unit
is compiled into the BCB component library. The Register procedure has
no impact on programs compiled with this unit. Unless your program calls the
Register procedure (which it should never do), the code for the
Register procedure will never execute.
This method first creates an array consisting of the types of components you
want to register. Recall that __classid is one of the new pieces of
syntax added to the language. It is dis- cussed in Chapter 2, "Basic Facts About
C++Builder." After creating the array, you call RegisterComponents,
which takes the name of the page you want to use in the Component Palette in the
first parameter, the array of metaclasses in the second parameter, and the size
of the array in the third parameter. If the page does not exist, it will be
created automatically. Here is the declaration for RegisterComponents:
extern void __fastcall RegisterComponents(const
System::AnsiString Page,
System::TMetaClass* const * ComponentClasses, const int ComponentClasses_Size);
Recall that TMetaClass is declared in SysDefs.h and that it
represents a subset of TObject. You can get the metaclass of a VCL
object by calling the object's ClassType method.
If you look back at the source for the whole project, you will see that the
Register method is placed in its own namespace. This happens because
multiple declarations for methods with that name will occur throughout your
projects and, indeed, inside BCB itself. As a result, the compiler's need for
order must be satiated with a few discreetly placed namespaces. This whole
subject will come up again in just a moment, when I talk about recompiling
CmpLib32.ccl.
After using the Component Expert, you should save the project. Proceed as you
normally would by creating a directory for the project and saving Main.pas
and TestUnleash1.cpp inside it. You should not save the new unit that
you created into the same directory, however, but you should place it in the
Utils directory where you store files such as CodeBox. This code
is now going to come into play as part of your system, and as such you want a
single path that leads to all related files of this type. If you have all your
components in different subdirectories, you will end up with a source path that
is long and unwieldy.
NOTE: ValidCtrCheck() verifies
that creating an instance of the object at compile time is possible. If you
forget to override a pure virtual function, the compilation of the component
will fail because the statement "new foo(0)" is now invalid. You can
test this by creating a component derived from TCustomGrid (an
abstract class) and installing it without doing any work to change the object.
Because ValidCtrCheck is inline and never called, no code will be
generated--so there is no runtime cost.
The goal of this project is to give a component of type TEdit a new set
of default behaviors so that it starts out with certain colors and certain
fonts. To do so, you need to override the Create method and change the
fonts inside it. The method declaration itself appears automatically:
class TSmallEdit : public TEdit
{
public:
virtual __fastcall TSmallEdit(TComponent* Owner);
};
Notice that in the preceding declaration I have removed the private,
__published, and protected directives created by the
Component Expert. Making this change is neither here nor there; I do it just to
keep the amount of code you need to look at as small as possible.
The Create method for TSmallEdit is declared as public.
If you think about the process of creating a component dynamically, you will see
that the Create method has to be public. This is one method
that must be exposed. Like all VCL constructors, TSmallEdit is declared
virtual and __fastcall.
Create is passed a single parameter of type TComponent,
which is a base class that encapsulates the minimum functionality needed to be
an owner of another component and to place a component on the Component Palette.
In particular, whatever form you place a component on usually will be the owner
of that component.
The implementation of the Create method is simple:
__fastcall TSmallEdit::TSmallEdit(TComponent* Owner)
: TEdit(Owner)
{
Color = clBlue;
Font->Color = clYellow;
Font->Name = "Times New Roman";
Font->Size = 12;
Font->Style = TFontStyles() << fsBold << fsItalic;
}
The code first calls Create, passing in the variable AOwner.
As I stated previously, the owner of a component will often, though not always,
be the form on which the component is to be displayed. In other words, the user
will drop the component onto a form, and that form will become the owner of the
component. In such a case, AOwner is a variable that points to the
form. The VCL uses it to initialize the Owner property, which is one of
the fields of all components.
The next step is to define the color and font that you want to use, with
Font->Style defined as follows:
enum TFontStyle { fsBold, fsItalic, fsUnderline,
fsStrikeOut };
typedef Set<TFontStyle, fsBold, fsStrikeOut> TFontStyles;
NOTE: Don't let all this gobbledygook
confuse you! All that occurs in this declaration is that a simple enum
type is declared, and then you see a Set declaration for the type
that ranges from a low value of fsBold to a high value of
fsStrikeOut. Piece of cake!
If you want to add the underline and bold style to the text in the edit
control, write the following:
Font->Style = TFontStyles << fsBold << fsUnderline;
If you then want to add the italic style at runtime, write this:
Font->Style = Font->Style << fsItalic;
To remove the style, write this line:
Font->Style = Font->Style >> fsItalic;
At this stage, the code is ready to go on the Component Palette. However,
most of the time when you write components, you should test them first to see
whether they work.
Even on a fast machine, the process of recompiling CmpLib32.ccl and
adding your component to the Component Palette takes between 10 seconds and 2
minutes. Perhaps I'm a bit impatient, but that's a little too long a wait for me
if all I need to tweak is a few aspects of my code. As a result, I test things
first in a small program and then add the component to the IDE.
To test the new class, drop a button on the program's main form, and create
an OnClick handler:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
TSmallEdit *MyEdit = new TSmallEdit(this);
MyEdit->Parent = this;
MyEdit->Show();
}
Don't forget to use View | Project Manager and add the unit to the project.
This ensures that the USEUNIT macro is placed in your project source.
This code creates the component and shows it on the main form. this,
of course, is the way that TForm1 refers to itself from inside one of
its own methods. The owner of the new component is Form1, which will be
responsible for disposing of the component when finished with it. This process
happens automatically. You never need to worry about disposing of a visible
component shown on a form.
The parent of the component is also Form1. The Parent
variable is used by Windows when it is trying to decide how to display the form
on the screen. If you place a panel on a form and drop a button on the panel,
the owner of that
button is the form, but the parent is the panel. Ownership determines when
and how the component is deallocated, and parental relationships determine where
and how the component is displayed. Ownership is fundamentally a BCB issue,
whereas parental
relationships are primarily concerns of Windows.
NOTE: The next paragraph in this
section explains how to recompile CmpLib32.ccl. Remember that you
should always keep a backup copy of CmpLib32.ccl on your hard drive.
CmpLib32.ccl is stored in the ..\CBUILDER\BIN directory.
Beneath this directory, I have created another directory called BACK
where I keep a backup copy of CmpLib32.ccl. If worse comes to worst,
you can always copy the version of CmpLib32.ccl on your installation
CD back into the BIN subdirectory.
My personal experience is that everyone, sooner or later, makes a tasty
Mulligan's Stew out of a copy of CmpLib32.ccl. The worst-case
scenario is that you're on the road when this mess happens, and that you don't
have access to your installation disk. Don't let this situation happen to you.
Keep a backup copy of CmpLib32.ccl on your hard drive at all times!
?FIGURE 17.16. Creating SQL selection criteria in ReportSmith. CCL stands for
C++Builder Component Library.
After you run the program and test the component, the next step is to put it up
on the Component Palette. To do so, choose Components | Install and then click
the Add button. Browse through the directories until you find Unleash1.Cpp,
select OK, and then close the Install Components dialog by clicking OK. At this
point, a project called CmpLib32.cpp is compiled. This project creates
a huge DLL called CmpLib32.ccl, which contains all the components in
the Component Palette, all the component and property editors associated with
those components, the form designer part of the IDE, the experts, and other
support modules.
After CmpLib32.ccl finishes recompiling, you can start a new
project, turn to the newly created Unleash page, and drop your new component
onto a form. It will have a blue background, default to Times New Roman, and
have its font style set to bold and its font color to yellow. Notice that all
the properties of TEdit have been inherited by TSmallEdit.
That's OOP in action. You can see an example of the form that uses this control
in Figure 22.3.
Figure 22.3.Using the TSmallEdit control on a standard VCL form.
Understanding the Project Source for CmpLib32
You can save the CmpLib32.cpp file used during compilation of
CmpLib32.ccl to disk if you choose Options | Environment | Library | Save
Library Source Code. After choosing this option and recompiling CmpLib32.ccl,
you can go to the \CBUILDER\BIN directory and view your copy of
CmpLib32.cpp.
Here is an example of the source code produced by the IDE for use when
compiling CmpLib32.ccl:
//----------------------------------
// Component Palette
// Copyright (c) 1996, 1996 by Borland International, All Rights Reserved
//
// Module generated by C++Builder to rebuild the Component
// Palette Library (CmpLib32.ccl).
//----------------------------------
//----------------------------------
// In order to be totally Delphi compatible, the Component palette must be
// built with a namespaced version of the VCL library. Using namespace allows
// us to have multiple functions all called `Register'.
//----------------------------------
#if !defined(BCB_NAMESPACES)
#define BCB_NAMESPACES
#endif
//----------------------------------
// Include DSTRING.H - Defines AnsiString support class
//----------------------------------
#include <vcl\dstring.h>
//----------------------------------
// The following are expanded inline to avoid pulling in the headers
// and lengthen the compilation time when rebuilding the palette.
//----------------------------------
typedef void* HINSTANCE;
#if !defined(WINAPI)
#define WINAPI __stdcall
#endif
#if !defined(DLL_PROCESS_ATTACH)
#define DLL_PROCESS_ATTACH 1
#endif
#if !defined(DLL_THREAD_ATTACH)
#define DLL_THREAD_ATTACH 2
#endif
#if !defined(DLL_THREAD_DETACH)
#define DLL_THREAD_DETACH 3
#endif
#if !defined(DLL_PROCESS_DETACH)
#define DLL_PROCESS_DETACH 0
#endif
namespace Libmain {
typedef void __fastcall (*TRegisterProc)(void);
extern void __fastcall RegisterModule(const System::AnsiString Name,
TRegisterProc RegisterProc);
}
using namespace Libmain;
namespace System {
extern void __cdecl ProcessAttachTLS(void);
extern void __cdecl ProcessDetachTLS(void);
extern void __cdecl ThreadAttachTLS(void);
extern void __cdecl ThreadDetachTLS(void);
}
using namespace System;
//----------------------------------
// Prototype for each component's `Register routine. Followed
// by instruction to have linker pull in the OBJ. module which
// implements the Register routine.
//
// Each component is expected to provide a routine with the
// following signature:
//
// extern void __fastcall Register(void);
//
// This routine must be in a namespace which matches the
// name of the component itself. Therefore, the routine is
// actually prototyped as:
//
// namespace Componentname {
// extern void __fastcall Register(void);
// };
//
// Note The namespace must be in all lowercase characters
// except for the first one. i.e. Namespacename.
//----------------------------------
namespace Stdreg { extern void __fastcall Register(void); }
namespace Dbreg { extern void __fastcall Register(void); }
namespace Isp { extern void __fastcall Register(void); }
namespace Sysreg { extern void __fastcall Register(void); }
namespace Quickrep { extern void __fastcall Register(void); }
namespace Ocxreg { extern void __fastcall Register(void); }
namespace Olereg { extern void __fastcall Register(void); }
namespace Ddereg { extern void __fastcall Register(void); }
namespace Chartfx { extern void __fastcall Register(void); }
namespace Vcfimprs { extern void __fastcall Register(void); }
namespace Vcfrmla1 { extern void __fastcall Register(void); }
namespace Vcspell { extern void __fastcall Register(void); }
namespace Graphsvr { extern void __fastcall Register(void); }
namespace Ibreg { extern void __fastcall Register(void); }
namespace Win31reg { extern void __fastcall Register(void); }
namespace Sampreg { extern void __fastcall Register(void); }
namespace Unleash1 { extern void __fastcall Register(void); }
// (Search Path for Components (.CPP, .PAS, .OBJ & .LIB)
// => g:\bcb\LIB;g:\bcb\LIB\OBJ;g:\srcc\punleash\utils
//
// Added to search paths: g:\bcb\LIB
// Added to search paths: g:\bcb\LIB\OBJ
// Added to search paths: g:\srcc\punleash\utils
// Added to project: g:\bcb\bin\cmplib32.cpp FileType: SRC
// Added to project: bcbmm.lib FileType: LIB
#pragma resource "StdReg.dcr" // Link dcr of standard module "StdReg"
// Added to project: g:\bcb\LIB\OBJ\DBReg.obj (From SearchPath) FileType: OBJ
// Added to project: g:\bcb\LIB\OBJ\DBReg.dcr (From SearchPath) FileType: RES
#pragma resource "ISP.dcr" // Link dcr of standard module "ISP"
#pragma resource "SysReg.dcr" // Link dcr of standard module "SysReg"
#pragma resource "Quickrep.dcr" // Link dcr of standard module "Quickrep"
#pragma resource "OLEReg.dcr" // Link dcr of standard module "OLEReg"
#pragma resource "DDEReg.dcr" // Link dcr of standard module "DDEReg"
#pragma resource "ChartFX.dcr" // Link dcr of standard module "ChartFX"
#pragma resource "VCFImprs.dcr" // Link dcr of standard module "VCFImprs"
#pragma resource "VCFrmla1.dcr" // Link dcr of standard module "VCFrmla1"
#pragma resource "VCSpell.dcr" // Link dcr of standard module "VCSpell"
#pragma resource "GraphSvr.dcr" // Link dcr of standard module "GraphSvr"
#pragma resource "IBReg.dcr" // Link dcr of standard module "IBReg"
#pragma resource "Win31Reg.dcr" // Link dcr of standard module "Win31Reg"
#pragma resource "SampReg.dcr" // Link dcr of standard module "SampReg"
// Added to project: g:\srcc\punleash\utils\Unleash1.cpp
// (From SearchPath) FileType: SRC
//----------------------------------
// Routine which registers the various modules implementing components
//----------------------------------
bool
InitCmpLib()
{
RegisterModule("StdReg", Stdreg::Register);
RegisterModule("DBReg", Dbreg::Register);
RegisterModule("ISP", Isp::Register);
RegisterModule("SysReg", Sysreg::Register);
RegisterModule("Quickrep", Quickrep::Register);
RegisterModule("OCXReg", Ocxreg::Register);
RegisterModule("OLEReg", Olereg::Register);
RegisterModule("DDEReg", Ddereg::Register);
RegisterModule("ChartFX", Chartfx::Register);
RegisterModule("VCFImprs", Vcfimprs::Register);
RegisterModule("VCFrmla1", Vcfrmla1::Register);
RegisterModule("VCSpell", Vcspell::Register);
RegisterModule("GraphSvr", Graphsvr::Register);
RegisterModule("IBReg", Ibreg::Register);
RegisterModule("Win31Reg", Win31reg::Register);
RegisterModule("SampReg", Sampreg::Register);
RegisterModule("Unleash1", Unleash1::Register);
return true;
}
//----------------------------------
// Library's entry point
//----------------------------------
extern "C"
int WINAPI
DllEntryPoint(HINSTANCE /*hInstance*/, unsigned long reason, void*)
{
switch (reason) {
case DLL_PROCESS_ATTACH:
ProcessAttachTLS();
InitCmpLib();
break;
case DLL_PROCESS_DETACH:
ProcessDetachTLS();
break;
case DLL_THREAD_ATTACH:
ThreadAttachTLS();
break;
case DLL_THREAD_DETACH:
ThreadDetachTLS();
break;
}
return 1;
}
To understand the unit, start at the bottom, with the DllEntryPoint
function. This routine is called when Windows first loads the DLL into memory
and each time it is accessed by a BCB thread. ProcessAttachTLS and
related routines are all part of the internal system code. This code is none of
our business, so we can safely "pay no attention to the man behind the curtain."
InitCmpLib calls the Register method for each of the
modules used in the project. If you look at the bottom of the list, you will see
where the
Register method for Unleash1 is listed. You might think
that we don't have enough calls in this section to create the over 100
components found on the Component Palette. The issue here is that some of the
Register methods
register 20 or 30 different components at one shot. Here, for example, is the
Register method from StdReg.pas:
procedure Register;
begin
RegisterComponents(LoadStr(srStandard), [TMainMenu, TPopupMenu, TLabel,
TEdit, TMemo, TButton, TCheckBox, TRadioButton, TListBox, TComboBox,
TScrollBar, TGroupBox, TRadioGroup, TPanel]);
RegisterComponents(LoadStr(srAdditional), [TBitBtn, TSpeedButton,
TMaskEdit, TStringGrid, TDrawGrid, TImage, TShape, TBevel,
TScrollBox]);
RegisterComponents(LoadStr(srWin95), [TTabControl, TPageControl,
TTreeView, TListView, TImageList, THeaderControl, TRichEdit,
TStatusBar, TTrackBar, TProgressBar, TUpDown, THotKey]);
RegisterClasses([TTabSheet]);
RegisterNoIcon([TMenuItem]);
RegisterComponentEditor(TMenu, TMenuEditor);
RegisterComponentEditor(TImage, TGraphicEditor);
RegisterComponentEditor(TPageControl, TPageControlEditor);
RegisterComponentEditor(TTabSheet, TPageControlEditor);
RegisterComponentEditor(TImageList, TImageListEditor);
RegisterPropertyEditor(TypeInfo(string),
TCustomMaskEdit, `EditMask', TMaskProperty);
RegisterPropertyEditor(TypeInfo(string),
TCustomMaskEdit, `Text', TMaskTextProperty);
RegisterPropertyEditor(TypeInfo(TTabSheet), TPageControl, `ActivePage',
TActivePageProperty);
RegisterPropertyEditor(TypeInfo(TStatusPanels), nil, `',
TStatusPanelsProperty);
RegisterPropertyEditor(TypeInfo(THeaderSections), nil, `',
THeaderSectionsProperty);
RegisterPropertyEditor(TypeInfo(TListColumns), nil, `',
TListColumnsProperty);
RegisterPropertyEditor(TypeInfo(TListItems), nil, `',
TListItemsProperty);
RegisterPropertyEditor(TypeInfo(TTreeNodes), nil, `',
TTreeNodesProperty);
end;
Sorry about bringing Object Pascal code into the book again, but I think it
helps to see what is happening behind the scenes in cases like this one. I will
talk about RegisterPropertyEditor later in this chapter. Notice that
the VCL calls
RegisterComponents just as you do. When you recompile Cmplib,
you are rebuilding part of BCB itself and simultaneously redefining the way the
VCL works.
The DCR files listed in CmpLib32.cpp are a series of standard
Windows resources that contain bitmaps that are placed in the Component Palette.
The bitmaps representing each component come from here. In the case of
TSmallEdit, it descends from TEdit, and so it inherits TEdit's
bitmap, unless I create a DCR file for the component. I will create and discuss
DCR files later in this chapter and in the next chapter.
The key point to grasp about DCR files is that they are standard Windows
resources containing a bitmap. You can build them with the Image Editor that
ships with BCB.
Extending the Unleash Unit
In the second version of the Unleash unit, a new edit control is
added, along with two labels and two panels. The additional edits and labels
show how quickly you can build on an idea or object when you understand where
you're headed. One of the panels shows how you can get rid of the annoying label
that always shows up in the middle of a panel, and the other shows a preliminary
take on how you can create a single component that contains other components.
Specifically, it shows how to create a panel that comes already equipped with
two radio buttons.
The code for the new version of the Unleash2.cpp unit is shown in
Listings 22.6 and 22.7, and its test bed appears in Listing 22.8. Run
TestBed first, and make sure the new components are working correctly. If
all is well, then add the new components to the Component Palette.
If you have two units on your system, both of which contain instances of
TSmallEdit, uninstalling the first instance before trying to install the
new instance is probably best. For instance, you may have two files on your
system, one called Unleash1.cpp and the second called Unleash2.cpp.
Both might contain an instance of TSmallEdit. Under such circumstances,
it's best to use Component | Install | Remove to remove the first version of
TSmallEdit before replacing it with a second version. A second alternative
is just to rename the second version of the component to TSmallEditTwo,
which is what I do with the source found on the CD that accompanies this book.
If you have only one version of TSmallEdit, and you just want to update
it, you don't need to remove the first instance before installing the updated
instance.
Listing 22.6. The header for the second version of the
Unleash2.cpp unit contains a class that comes equipped with two radio
buttons.
///////////////////////////////////////
// Unleash2.h
// Simple components
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef Unleash2H
#define Unleash2H
//----------------------------------
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\StdCtrls.hpp>
//----------------------------------
class TSmallEditTwo : public TEdit
{
public:
virtual __fastcall TSmallEditTwo(TComponent* Owner);
};
class TBigEdit : public TSmallEditTwo
{
public:
virtual __fastcall TBigEdit(TComponent* Owner)
:TSmallEditTwo(Owner) { Width = 250; Font->Size = 24; }
};
class TSmallLabel : public TLabel
{
public:
virtual __fastcall TSmallLabel(TComponent* Owner);
};
class TBigLabel : public TSmallLabel
{
public:
virtual __fastcall TBigLabel(TComponent *Owner)
:TSmallLabel(Owner) { Font->Size = 24; }
};
class TEmptyPanel : public TPanel
{
protected:
void __fastcall Loaded(void);
public:
virtual __fastcall TEmptyPanel(TComponent *Owner);
virtual void __fastcall SetParent(TWinControl *AParent);
};
class TRadio2Panel : public TEmptyPanel
{
private:
TRadioButton *FRadio1;
TRadioButton *FRadio2;
public:
virtual __fastcall TRadio2Panel(TComponent *Owner);
__property TRadioButton *Radio1={read=FRadio1};
__property TRadioButton *Radio2={read=FRadio2};
};
//----------------------------------
#endif
Listing 22.7. The second version of the Unleash unit,
called Unleash2, contains a class that comes equipped with two radio buttons.
///////////////////////////////////////
// Unleash2.cpp
// Simple components
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Unleash2.h"
///////////////////////////////////////
// TSmallEditTwo //////////////////////
///////////////////////////////////////
static inline TSmallEditTwo *ValidCtrCheck()
{
return new TSmallEditTwo(NULL);
}
__fastcall TSmallEditTwo::TSmallEditTwo(TComponent* Owner)
: TEdit(Owner)
{
Color = clBlue;
Font->Color = clYellow;
Font->Name = "Times New Roman";
Font->Size = 12;
Font->Style = TFontStyles() << fsBold << fsItalic;
}
///////////////////////////////////////
// TSmallLabel ////////////////////////
///////////////////////////////////////
__fastcall TSmallLabel::TSmallLabel(TComponent* Owner)
: TLabel(Owner)
{
Color = clBlue;
Font->Color = clYellow;
Font->Name = "Times New Roman";
Font->Size = 12;
Font->Style = TFontStyles() << fsBold;
}
///////////////////////////////////////
// TEmptyPanel ////////////////////////
///////////////////////////////////////
__fastcall TEmptyPanel::TEmptyPanel(TComponent *Owner)
:TPanel(Owner)
{
}
void __fastcall TEmptyPanel::Loaded(void)
{
TPanel::Loaded();
Caption = "";
}
void __fastcall TEmptyPanel::SetParent(TWinControl *AParent)
{
TPanel::SetParent(AParent);
Caption = "";
}
///////////////////////////////////////
// TRadio2Panel ///////////////////////
///////////////////////////////////////
__fastcall TRadio2Panel::TRadio2Panel(TComponent *Owner)
:TEmptyPanel(Owner)
{
Width = 175;
Height = 60;
FRadio1 = new TRadioButton(this);
FRadio1->Parent = this;
FRadio1->Caption = "Radio1";
FRadio1->Left = 20;
FRadio1->Top = 10;
FRadio1->Show();
FRadio2 = new TRadioButton(this);
FRadio2->Parent = this;
FRadio2->Caption = "Radio2";
FRadio2->Left = 20;
FRadio2->Top = 32;
FRadio2->Show();
}
namespace Unleash2
{
void __fastcall Register()
{
TComponentClass classes[6]={__classid(TSmallEditTwo),
__classid(TBigEdit), __classid(TSmallLabel), __classid(TBigLabel),
__classid(TEmptyPanel), __classid(TRadio2Panel)};
RegisterComponents("Unleash", classes, 5);
}
}
//----------------------------------
Listing 22.8. The test bed for the Unleash2 unit.
///////////////////////////////////////
// Main.cpp
// Simple components: TestUnleash2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "Unleash2.h"
#pragma link "Unleash1"
#pragma link "Unleash2"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
TComponentClass classes[1]={__classid(TRadioButton)};
RegisterClasses(classes, 0);
}
void __fastcall TForm1::TestComponentsBtnClick(TObject *Sender)
{
TestComponentsBtn->Enabled = False;
TBigEdit *BigEdit = new TBigEdit(this);
BigEdit->Parent = this;
BigEdit->Show();
TEmptyPanel *E = new TEmptyPanel(this);
E->Parent = this;
E->Top = BigEdit->Height + 5;
E->Show();
TRadio2Panel *P = new TRadio2Panel(this);
P->Parent = this;
P->Top = E->Top + E->Height + 5;
P->Radio1->Caption = "As you from crimes would pardon'd be ";
P->Radio1->Width = Canvas->TextWidth(P->Radio1->Caption) + 20;
P->Radio2->Caption = "Let your indulgence set me free ";
P->Radio2->Width = Canvas->TextWidth(P->Radio2->Caption) + 20;
P->Width = max(P->Radio1->Width, P->Radio2->Width) + P->Radio1->Left + 20;
P->Show();
}
You can find this program in the directory called TestUnleash2 on the
CD that accompanies this book. Figure 22.4 shows the program in action.
Figure 22.4.The TestUnleash2 program demonstrates how to test a series of
components before placing them on the Component Palette.
After you've created a component that does something you like, you can easily
create children of it. Class TBigEdit descends from TSmallEditTwo:
class TBigEdit : public TSmallEditTwo
{
public:
virtual __fastcall TBigEdit(TComponent* Owner)
:TSmallEditTwo(Owner) { Width = 250; Font->Size = 24; }
};
It inherits its font nearly unchanged from TSmallEditTwo, except
that it sets Font->Size to 24, a hefty figure that helps the
control live up to its name. This elegant syntax is a good example of how OOP
can save you time and trouble while still allowing you to write clear code.
The label controls shown in this code work in exactly the same way the edit
controls do, except that they descend from TLabel rather than from
TEdit.
The TEmptyPanel component rectifies one of the petty issues that
sometimes annoys me: Every time you put down a panel, it gets a caption. Most of
the time, the first thing you do is delete the caption so that you can place
other controls on it without creating a mess!
At first, it would appear that you can change the caption of TPanel
by overriding its constructor. All you would need to do is set the Caption
property to an empty string:
class TEmptyPanel : public TPanel
{
public:
virtual __fastcall TEmptyPanel(TComponent *Owner)
:TPanel(Owner) { Caption = ""; }
};
This code should work correctly, but in the first version of C++Builder the
caption does not get changed. There are two interesting workarounds for this
problem that illustrate interesting facts about the VCL. The first workaround is
to override the Loaded method:
void __fastcall TEmptyPanel::Loaded(void)
{
TPanel::Loaded();
Caption = "";
}
The code shown in this overridden Loaded method will change the
caption of the component at runtime, but not at design time. The Loaded
method is called after a component has been loaded from a stream but before it
is shown to the user. The method exists so that you can set the value of
properties that rely on the value of other properties.
A second technique involves overriding the SetParent method:
void __fastcall TEmptyPanel::SetParent(TWinControl *AParent)
{
TPanel::SetParent(AParent);
Caption = "";
}
This will fix the problem at both design time and at runtime.
In this example, I am not using SetParent for its intended purpose.
To understand the method's real purpose, you need to remember that a component
is always shown on the surface of its parent. The primary purpose of the
SetParent method is to give you a chance to change the parent of a
component just as it is being made visible to the user. This allows you to store
a component in a DFM file with one parent, and to change that parent at runtime.
It's unlikely you will ever need to change the parent of a component, but there
are occasions when it is useful, and so the VCL gives you that ability.
It is obviously useful to know that you can override the SetParent
and
Loaded methods in order to change properties at two different stages
in the process of allocating memory for a component. You should note, however,
that in this case it is a bit strange that you have to do so, because merely
overriding the constructor should have turned the trick.
The last new component in Unleash2.cpp enables you to drop down a
panel that comes equipped with two radio buttons. This way, you can make a
single control out of a set of components that are often combined.
You could create other controls that contain three, four, or more radio
buttons. Or you could even create a panel that would populate itself with a
specific number of radio buttons.
The declaration for this new radio button is fairly simple:
class TRadio2Panel : public TEmptyPanel
{
private:
TRadioButton *FRadio1;
TRadioButton *FRadio2;
public:
virtual __fastcall TRadio2Panel(TComponent *Owner);
__property TRadioButton *Radio1={read=FRadio1 };
__property TRadioButton *Radio2={read=FRadio2 };
};
The actual radio buttons themselves are declared as private data, and access
to them is given by the Radio1 and Radio2 properties.
Write access to the radio button properties is not needed because you can
modify without it. The following statement performs one read of RP->Radio1
and one write to the Caption property of that radio button:
P->Radio1->Caption = "Control one";
You don't want write access to the properties either because that would allow
the user to assign them garbage (or NULL).
The constructor for the Radio2Panel begins by setting the width and
height of the panel:
__fastcall TRadio2Panel::TRadio2Panel(TComponent
*Owner)
:TEmptyPanel(Owner)
{
Width = 175;
Height = 60;
FRadio1 = new TRadioButton(this);
FRadio1->Parent = this;
FRadio1->Caption = "Radio1";
FRadio1->Left = 20;
FRadio1->Top = 10;
FRadio1->Show();
FRadio2 = new TRadioButton(this);
FRadio2->Parent = this;
FRadio2->Caption = "Radio2";
FRadio2->Left = 20;
FRadio2->Top = 32;
FRadio2->Show();
}
The next step is to create the first radio button. Notice that the code
passes
this as the owner and sets the parent to the panel itself. The rest
of the code in the Create method is too trivial to merit comment.
Under some circumstances, you may need to register TRadioButton:
TComponentClass classes[1]={__classid(TRadioButton)};
RegisterClasses(classes, 0);
This event would normally occur when you drop a component on a form. However,
in this case a TRadioButton is not necessarily ever dropped explicitly
on a form. As a result, the safe thing to do is register the component, possibly
in the constructor for the class.
When you're ready to test the TRadio2Panel object, you can write the
following code in the test-bed program to take it through its paces:
TRadio2Panel *P = new TRadio2Panel(this);
P->Parent = this;
P->Top = E->Top + E->Height + 5;
P->Radio1->Caption = "As you from crimes would pardon'd be ";
P->Radio1->Width = Canvas->TextWidth(P->Radio1->Caption) + 20;
P->Radio2->Caption = "Let your indulgence set me free ";
P->Radio2->Width = Canvas->TextWidth(P->Radio2->Caption) + 20;
P->Width = max(P->Radio1->Width, P->Radio2->Width) + P->Radio1->Left + 20;
P->Show();
Note that each radio button that belongs to the panel acts exactly as you
would expect a normal radio button to act, except you have to qualify it
differently before you access it. I use the TextWidth property of
Canvas to discover the width needed for the string, and then add 20 to take
into account the button itself.
NOTE: If you want, you can surface
Radio1 and Radio2 in the Object Inspecter as published
properties of TRadio2Panel. However, when you first do so, they will
have no property editors available because BCB has no built-in property
editors for TRadioButtons. To build your own, you can refer to the
DsgnIntf.pas unit that ships with BCB, as well as the upcoming
discussion of the Clock component and the Tools API.
The following code is used to register the objects in the Unleash2
unit so they can be placed on the Component Palette:
namespace Unleash2
{
void __fastcall Register()
{
TComponentClass classes[6]={__classid(TSmallEditTwo),
__classid(TBigEdit), __classid(TSmallLabel), __classid(TBigLabel),
__classid(TEmptyPanel), __classid(TRadio2Panel)};
RegisterComponents("Unleash", classes, 5);
}
}
Notice how I register multiple components in this example at once by creating
an array of type TComponentClass. After registering the objects, you
can place them on the Component Palette and use them in a program, as shown in
Figures 22.5 and 22.6.
Figure 22.5.The main form of a VCL application that uses some of the
components from the Unleash2 unit.
Figure 22.6.The new components on the Component Palette, along with some
other controls created in later chapters.
NOTE: To create custom bitmaps for
components shown on the Component Palette, you need to create a standard
resource with the extension .dcr. The Image Editor component that
ships with BCB is designed to handle this chore.
I ship a few DCR files in the Utils directory on the CD that
accompanies this book. You can study them to see how to proceed, or you can
find the DCR files for other components that ship with BCB. To look at a DCR
file, open it with the Image Editor.
The DCR file should have the name of the unit that contains the controls, and
each small 24x24 bitmap that you create in the file should be named after the
component to which it belongs. For example, the DCR file for this unit would
be called Unleash2.dcr, and the bitmaps inside it would have names
like TBIGEDIT and TEMPTYPANEL.
I will discuss this subject in more depth later in this chapter.
Before closing this section, I'd like to add some notes about how BCB handles
streaming chores. The good news is that most of the time you don't have to
concern yourself with streaming at all. BCB handles most streaming chores
automatically. In particular, it will automatically stream published
properties that are simple types. Only under limited circumstances must you
explicitly stream the fields of your object.
If a property type is a TComponent or descendant, the streaming
system assumes it must create an instance of that type when reading it in. If a
property type is TPersistent but not TComponent, the streaming
system assumes it is supposed to use the existing instance available through the
property and read values into that instance's properties.
The Object Inspector knows to expand the properties of TPersistent
but not TComponent descendants. This expansion is not done for
TComponent descendants because they are likely to have a lot more
properties, which would make navigating the Object Inspector difficult.
Building Components from Scratch
In the previous examples, you created descendants of existing components. Now
you're ready to see how to create entirely new components. The main idea to
grasp here is that you can make a new component descend from three abstract
objects. The term "abstract" can have a specific technical meaning, but here I'm
using it to refer to any object that exists only so that you can create
descendants of it. In short, the following three objects have built-in
functionality that all components need to access, but you would never want to
instantiate an instance of any of them:
- TWinControl and TCustomControl are base classes that can
be used to produce a Windows control that can receive input focus and that has
a standard Windows handle that can be passed to API calls. TWinControl
descendants exist inside their own window. TEdit, TListBox,
TTabbedNotebook, TNotebook, and TPanel are all
examples of this type of control. Most components of this type actually
descend from TCustomControl, which is in turn a descendant of
TWinControl. The distinction between the two classes is that
TCustomControl has a Paint method, and TWinControl does
not. If you want to draw the display of your new component, you should make it
inherit from TCustomControl. If the object already knows how to draw
itself, inherit from TWinControl.
- TGraphicControl is for components that don't need to receive
input focus, don't need to contain other components, and don't need a handle.
These controls draw themselves directly on their parent's surface, thereby
saving Windows resources. Not having a window handle eliminates a lot of
Windows management overhead, and that translates into faster display updates.
In short, TGraphicControls exist inside their parent's window. They
use their parent's handle and their parent's device context. They still have
Handle and Canvas fields that you can access, but they
actually belong to their parent. TLabel and TShape objects
are examples of this type of component. The drawback with this system is that
the component can never get the focus!
- TComponent enables you to create nonvisual components. If you
want to make a tool such as the TTable, TQuery,
TOpenDialog, or TTimer devices, this is the place to start. You
can place these components on the Component Palette, but they perform some
internal function that you access through code instead of appearing to the
user at runtime. A tool such as TOpenDialog can pop up a dialog, but
the component itself remains invisible.
Create a TWinControl or TCustomControl descendant whenever
the user needs to interact directly with a visible control. If the user doesn't
need to interact with a visible component, create a TGraphicControl
descendant.
To get a handle on the issues involved here, you should place a TShape
or TLabel control on a form and run the program. Clicking or attempting
to type on these controls produces no noticeable result. These components don't
ever receive the focus. Now place a TEdit control on the form. It
responds to mouse clicks, gets the focus, and you can type in it. TEdit
controls are descendants of TWinControl, and TShape is a
descendant of TGraphicControl.
NOTE: I should add one caveat to the
rules about TGraphicControl explained previously. In one limited
sense, the user can interact with a TGraphicControl. For example,
these controls do receive mouse messages, and you can set the mouse cursor
when the mouse flies over them. They just can't receive keyboard input focus.
If an object can't receive focus, it usually seems inert to the user.
If you're having trouble deciding whether you want to make descendants from
TWinControl or TCustomControl, you should always go with
TCustomControl. It has a real Paint method and some other
functionality that is useful when creating a component of your own. If you want
to wrap an existing Windows control inside a VCL object, you should start with
TWinControl. Most BCB components that follow this path begin by
creating intermediate custom objects, so the hierarchy of TEdit looks
like this:
TWinControl
TCustomEdit
TEdit
The hierarchy of TListBox looks like this:
TWinControl
TCustomListBox
TListBox
Of course, BCB wraps all the major Windows controls for you, so you won't
need to perform this operation unless you're working with a specialized
third-party control of some sort.
Following are the declarations for TGraphicControl and
TCustomControl, as they appear in Controls.hpp:
class __declspec(delphiclass) TGraphicControl;
class __declspec(pascalimplementation) TGraphicControl : public TControl
{
typedef TControl inherited;
private:
Graphics::TCanvas* FCanvas;
MESSAGE void __fastcall WMPaint(Messages::TWMPaint &Message);
protected:
virtual void __fastcall Paint(void);
__property Graphics::TCanvas* Canvas = {read=FCanvas, nodefault};
public:
__fastcall virtual TGraphicControl(Classes::TComponent* AOwner);
__fastcall virtual ~TGraphicControl(void);
};
class __declspec(delphiclass) TCustomControl;
class __declspec(pascalimplementation) TCustomControl : public TWinControl
{
typedef TWinControl inherited;
private:
Graphics::TCanvas* FCanvas;
MESSAGE void __fastcall WMPaint(Messages::TWMPaint &Message);
protected:
virtual void __fastcall Paint(void);
virtual void __fastcall PaintWindow(HDC DC);
__property Graphics::TCanvas* Canvas = {read=FCanvas, nodefault};
public:
__fastcall virtual TCustomControl(Classes::TComponent* AOwner);
__fastcall virtual ~TCustomControl(void);
public:
/* CreateParented */ __fastcall TCustomControl(HWND ParentWindow) :
Controls::TWinControl(ParentWindow) { }
};
You can see that these objects are fairly simple. If you go back one step
further in the hierarchy to TControl or TWinControl, you see
huge objects.
For example, the declaration for class TWinControl is nearly 200
lines long (not the implementation, mind you, just the type declaration).
I'm showing you this source code because component builders should work
directly with the source rather than use the online help or the docs. For simple
jobs, you can easily create your own components without the source. However, if
you have a big project, you have to get the source code if it did not ship with
your product. The INT files that ship with all versions of BCB are very helpful,
but you'll find no replacement for the actual source. The source is available
with the client/server version of BCB and also as an up-sell from Borland.
The Clock Component
You now know enough to build a component from the ground up. The CLOCK
unit, as shown in Figure 22.7, is a simple clock that you can pop onto a form,
and activate and deactivate at will. In particular, you can start the clock
running and then tell it to stop by changing the value of a Boolean property
called Running.
Figure 22.7.The CLOCK unit as it appears on its own test bed,
before being placed on the Component Palette.
When you're constructing class TClock, the first thing you need to
decide is whether the clock is going to descend from TWinControl or
TGraphicControl. If you've built a clock in Windows before, you know that
one of the best ways to drive it is with a Windows timer. Timers require the
presence of a handle so that they can be stopped and started; furthermore, they
send their WM_TIMER messages to the window that owns them. Because a
TGraphicControl descendant isn't a real window, it will not
automatically get the messages. As a result, TGraphicControl is not an
ideal choice for this type of object.
Of course, the objections to using TGraphicControl raised in the
preceding paragraph aren't insurmountable. If you really want to, you can still
make your clock work this way. However, there's no point in expending effort
that isn't strictly necessary, so I have opted for the simplest design possible
and have made the class a descendant of TCustomControl. I chose
TCustomControl rather than TWinControl because I needed a
Paint method in which I could draw the clock. (See Figure 22.8.)
Figure 22.8.The TClock and TColorClock controls shown
on a form.
The code, shown in Listings 22.9 through 22.11, also contains a special
property editor, as well as a simple component editor. As you will see, neither
of these tools is inherently difficult to build.
NOTE: Before installing the clock
components, you might need to run the BuildObjs program found in the Utils
directory. See the readme file on the CD that accompanies this chapter for
additional information.
Listing 22.9. The header for the Clock component
contains declarations for the clock and its property and component editors.
///////////////////////////////////////
// Clock1.h
// Clock Component
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef Clock1H
#define Clock1H
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\dsgnintf.hpp>
#include <vcl\messages.hpp>
#include <vcl\extctrls.hpp>
class TClockAttributes: public TPersistent
{
private:
TColor FColor;
TShapeType FShape;
TComponent *FOwner;
void __fastcall SetColor(TColor AColor);
void __fastcall SetShape(TShapeType AShape);
public:
virtual __fastcall TClockAttributes() : TPersistent() { }
virtual __fastcall TClockAttributes(TComponent *AOwner)
: TPersistent() { FOwner = AOwner; }
__published:
__property TColor Color={read=FColor, write=SetColor};
__property TShapeType Shape={read=FShape, write=SetShape};
};
////////////////////////////////////////
// TMyClock ////////////////////////////
////////////////////////////////////////
class TMyClock: public TCustomControl
{
private:
int FTimer;
Boolean FRunning;
TClockAttributes *FClockAttributes;
void __fastcall SetRunning(Boolean ARun);
protected:
virtual __fastcall ~TMyClock() { delete FClockAttributes; }
virtual void __fastcall Paint(void);
void __fastcall WMTimer(TMessage &Message);
void __fastcall WMDestroy(TMessage &Message);
public:
virtual __fastcall TMyClock(TComponent* Owner);
__published:
__property Boolean Running={read=FRunning, write=SetRunning};
__property Align;
__property TClockAttributes *ClockAttributes=
{read=FClockAttributes, write=FClockAttributes };
__property Font;
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_TIMER, TMessage, WMTimer);
MESSAGE_HANDLER(WM_DESTROY, TMessage, WMDestroy);
END_MESSAGE_MAP(TCustomControl);
};
////////////////////////////////////////
// TColorClock /////////////////////////
////////////////////////////////////////
class TColorClock: public TMyClock
{
private:
TColor FFaceColor;
void __fastcall SetColor(TColor Color);
protected:
void virtual __fastcall Paint(void);
public:
virtual __fastcall TColorClock(TComponent *Owner)
:TMyClock(Owner) { FFaceColor = clGreen; }
__published:
__property TColor FaceColor={read=FFaceColor, write=SetColor, nodefault};
};
////////////////////////////////////////
// TClockEditor ////////////////////////
////////////////////////////////////////
class TClockEditor: public TComponentEditor
{
protected:
virtual __fastcall void Edit(void);
public:
virtual __fastcall TClockEditor(TComponent *AOwner, TFormDesigner *Designer)
: TComponentEditor(AOwner, Designer) {}
};
////////////////////////////////////////
// TColorNameProperty /////////////////
////////////////////////////////////////
class TColorNameProperty: public TClassProperty
{
public:
TColorNameProperty(): TClassProperty() {}
TPropertyAttributes __fastcall GetAttributes(void);
};
#endif
Listing 22.10. The code for the Clock component should
be kept in the Utils subdirectory where you store CodeBox and other utility
units.
///////////////////////////////////////
// Clock1.cpp
// Clock Component
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Clock1.h"
///////////////////////////////////////
// ValidControlCheck
///////////////////////////////////////
static inline TMyClock *ValidCtrCheck()
{
return new TMyClock(NULL);
}
///////////////////////////////////////
// TColorsProperty
///////////////////////////////////////
void __fastcall TClockAttributes::SetColor(TColor AColor)
{
FColor = AColor;
((TControl *)(FOwner))->Invalidate();
};
void __fastcall TClockAttributes::SetShape(TShapeType AShape)
{
FShape = AShape;
((TControl *)(FOwner))->Invalidate();
};
///////////////////////////////////////
// Constructor
///////////////////////////////////////
__fastcall TMyClock::TMyClock(TComponent* Owner)
: TCustomControl(Owner)
{
Width = 100;
Height = 100;
FTimer = 1;
FClockAttributes = new TClockAttributes(this);
FClockAttributes->Color = clBtnFace;
FClockAttributes->Shape = stEllipse;
}
///////////////////////////////////////
// SetRunning
///////////////////////////////////////
void __fastcall TMyClock::SetRunning(Boolean Run)
{
if (Run)
{
SetTimer(Handle, FTimer, 50, NULL);
FRunning = True;
}
else
{
KillTimer(Handle, FTimer);
FRunning = False;
}
}
///////////////////////////////////////
// Paint
///////////////////////////////////////
void __fastcall TMyClock::Paint(void)
{
Color = ClockAttributes->Color;
switch (ClockAttributes->Shape)
{
int X;
case stEllipse: Canvas->Ellipse(0, 0, Width, Height); break;
case stRectangle: Canvas->Rectangle(0, 0, Width, Height); break;
case stRoundRect:
Canvas->RoundRect(0, 0, Width, Height, Width - 100, Height);
break;
case stSquare:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Rectangle(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Rectangle((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
case stCircle:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Ellipse(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Ellipse((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
}
}
///////////////////////////////////////
// WM_TIMER
///////////////////////////////////////
void __fastcall TMyClock::WMTimer(TMessage &Message)
{
AnsiString S = TimeToStr(Time());
Canvas->Font = Font;
Canvas->TextOut(Width / 2 - Canvas->TextWidth(S) / 2,
Height / 2 - Canvas->TextHeight(S) / 2, S);
}
///////////////////////////////////////
// WM_DESTROY
///////////////////////////////////////
void __fastcall TMyClock::WMDestroy(TMessage &Message)
{
KillTimer(Handle, FTimer);
FTimer = 0;
TCustomControl::Dispatch(&Message);
}
//------------------------------------
//-- TColorClock --------------------
//------------------------------------
///////////////////////////////////////
// WM_DESTROY
///////////////////////////////////////
void __fastcall TColorClock::Paint()
{
Canvas->Brush->Color = FFaceColor;
TMyClock::Paint();
}
///////////////////////////////////////
// SetColor
///////////////////////////////////////
void __fastcall TColorClock::SetColor(TColor Color)
{
FFaceColor = Color;
InvalidateRect(Handle, NULL, True);
}
//------------------------------------
//-- TClockEditor --------------------
//------------------------------------
void __fastcall TClockEditor::Edit(void)
{
TColorDialog *C = new TColorDialog(Application);
if (C->Execute())
((TColorClock *)(Component))->FaceColor = C->Color;
}
//------------------------------------
//-- TColorNameProperty --------------
//------------------------------------
TPropertyAttributes __fastcall TColorNameProperty::GetAttributes(void)
{
return TPropertyAttributes() << paSubProperties;
}
///////////////////////////////////////
// Register
///////////////////////////////////////
namespace Clock1
{
void __fastcall Register()
{
TComponentClass classes[2] = {__classid(TMyClock), __classid(TColorClock)};
RegisterComponents("Unleash", classes, 1);
RegisterComponentEditor(__classid(TColorClock), __classid(TClockEditor));
RegisterPropertyEditor(__typeinfo(TClockAttributes),
__classid(TMyClock), "Colors", __classid(TColorNameProperty));
}
}
Listing 22.11. The test bed for the clock component is
stored in the TestClock subdirectory.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "Clock1.h"
#pragma link "Clock1"
#pragma link "sampreg"
#pragma link "ClockProperty1"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Create1Click(TObject *Sender)
{
MyClock = new TColorClock(this);
MyClock->Parent = this;
MyClock->Show();
}
void __fastcall TForm1::ToggleRunning1Click(TObject *Sender)
{
MyClock->Running = !MyClock->Running;
}
void __fastcall TForm1::SetColor1Click(TObject *Sender)
{
if (ColorDialog1->Execute())
{
MyClock->FaceColor = ColorDialog1->Color;
}
}
To run this program, you should first select the menu item that creates the
clock and makes it visible on the form. The next logical step is to start the
clock running; then, if you'd like, you can also change its color. You get an
Access Violation if you click the latter two buttons before selecting
the first. The problem is that calling a method or property of the TClock
object before the object itself has been created is an error. To prevent this
error from happening, you could enable and disable the second two buttons.
The code for the clock components uses inheritance, virtual methods, and
properties. TClock has two pieces of private data:
int FTimer;
Boolean FRunning;
One is an identifier for the timer, and the other is a Boolean value that
specifies whether the clock is running.
Windows timers are managed by two Windows API calls. When you want to start a
timer, use SetTimer; when you want to stop a timer, use KillTimer.
SetTimer takes four parameters:
- Handle is the HWND of your current window.
- IDEvent is an integer identifier that uniquely identifies the
timer inside the window that created it. You can make up this value off the
top of your head, although I generally set the IDTimer for the first
timer in a window to 1, the second timer to 2, and so on.
Because you're going to have only one timer in each instance of a TClock
window, you can set its IDEvent to 1.
- Elapse is the length of time between calls to the timer, measured
in milliseconds.
- TimerFunc is a callback function that is not used in this
program. One of the developers' big goals was to create a Windows product that
didn't need to use callbacks, and I see no reason to open that can of worms
now if I can avoid it. (If you want to create a callback in BCB, you will be
able to do so, but it's usually not necessary.)
A typical call to SetTimer looks like this:
SetTimer(Handle, FTimer, 50, NULL);
50 specifies that the timer is called once every 50 milliseconds.
SetTimer returns zero if the call fails. This is a real possibility, so
programs that include error checking should inspect this value and put up a
MessageBox if the call fails.
KillTimer takes two parameters, the first being the handle of your
window and the second being the unique identifier associated with that timer:
KillTimer(Handle, FTimer);
When you're not using the callback function, timer events are sent to your
window by way of messages:
void __fastcall WMTimer(TMessage &Message);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_TIMER, TMessage, WMTimer);
MESSAGE_HANDLER(WM_DESTROY, TMessage, WMDestroy);
END_MESSAGE_MAP(TCustomControl);
This classic dynamic method is of the kind covered in Chapter 4, "Events."
The response to this event is a simple procedure that calls TextOut and
gets the time from the VCL:
void __fastcall TMyClock::WMTimer(TMessage &Message)
{
AnsiString S = TimeToStr(Time());
Canvas->TextOut(Width / 2 - Canvas->TextWidth(S) / 2,
Height / 2 - Canvas->TextHeight(S) / 2, S);
}
The calls to SetTimer and KillTimer are managed primarily
through a property called Running:
__property Boolean Running={read=FRunning,
write=SetRunning};
The write mechanism, a procedure called SetRunning, is a fairly
straightforward tool:
void __fastcall TMyClock::SetRunning(Boolean Run)
{
if (Run)
{
SetTimer(Handle, FTimer, 50, NULL);
FRunning = True;
}
else
{
KillTimer(Handle, FTimer);
FRunning = False;
}
}
If the user sets the Running property to True, this
procedure is executed and a call is made to the SetTimer function. If
the user sets Running to False, KillTimer is called
and the clock immediately stops functioning.
The final issue involving the timer concerns the case in which the user
closes a form while the clock is still running. In such a case, you must be sure
to call KillTimer before the application exits. If you don't make the
call, the timer keeps running even after the application closes. Having the
timer running wastes system resources and also uses up one of the dozen or so
timers that are available to the system at any one time.
The logical place to call KillTimer is the Destroy method
for the TClock object. Unfortunately, the window associated with the
clock has already been destroyed by the time this call is made, so no valid
handle is available for use when you call KillTimer. As a result, you
need to respond to WM_DESTROY messages to be sure the timer is killed
before the TClock window is closed:
void __fastcall TMyClock::WMDestroy(TMessage &Message)
{
KillTimer(Handle, FTimer);
FTimer = 0;
TCustomControl::Dispatch(&Message);
}
Before leaving this description of the TClock object, I should
briefly mention the Paint method. Here is the simplest possible version
of the paint procedure for the program:
void __fastcall TMyClock::Paint(void)
{
Canvas->Ellipse(0, 0, 100, 100);
}
This procedure is called whenever the circle defining the circumference of
the clock needs to be repainted. You never have to check for this circumstance,
and you never have to call Paint directly. Windows keeps an eye on the
TClock window, and if the window needs to be painted, Windows sends you
a WM_PAINT message. Logic buried deep in the VCL converts the
WM_PAINT message into a call to Paint, the same way TClock
translates WM_TIMER messages into calls to TCanvas->TextOut.
The actual paint procedure used in the program looks like this:
///////////////////////////////////////
// Paint
///////////////////////////////////////
void __fastcall TMyClock::Paint(void)
{
Color = ClockAttributes->Color;
switch (ClockAttributes->Shape)
{
int X;
case stEllipse: Canvas->Ellipse(0, 0, Width, Height); break;
case stRectangle: Canvas->Rectangle(0, 0, Width, Height); break;
case stRoundRect:
Canvas->RoundRect(0, 0, Width, Height, Width - 100, Height);
break;
case stSquare:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Rectangle(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Rectangle((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
case stCircle:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Ellipse(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Ellipse((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
}
}
This version of the Paint procedure takes into account the
ClockAttributes property of TMyClock. TClockAttributes
looks like this:
class TClockAttributes: public TPersistent
{
private:
TColor FColor;
TShapeType FShape;
TComponent *FOwner;
void __fastcall SetColor(TColor AColor);
void __fastcall SetShape(TShapeType AShape);
public:
virtual __fastcall TClockAttributes() : TPersistent() { }
virtual __fastcall TClockAttributes(TComponent *AOwner)
: TPersistent() { FOwner = AOwner; }
__published:
__property TColor Color={read=FColor, write=SetColor};
__property TShapeType Shape={read=FShape, write=SetShape};
};
As you can see, it provides a shape and a background color for the clock. The
most important aspect of the TClockAttributes type is the property
editors I create for it. These editors let you access both properties of this
class through a series of special techniques, described later in this chapter.
For now, you might want to merely click the ClockAttributes property,
open up its two fields,
and see how it works.
The TColorClock component, which is a descendant of TClock,
adds color to the face of the control. I made TColorClock a separate
object, instead of just adding a face color to TClock, for two
different reasons (both of which are related to design):
- You might want to create a descendant of TClock that doesn't have
color or one that implements color differently than TColorClock does.
By creating two objects, one called TClock and the other called
TColorClock, I enable programmers to have the greatest amount of freedom
when creating descendants. This principle has only minimal weight in a simple
object such as TClock, but it can become extremely important when
you're developing large and complex hierarchies. In short, be careful of
building too much functionality into one object!
- TClock and TColorClock also provide another example of
inheritance and vividly demonstrate how this technology can be utilized to
your advantage.
TColorClock declares a private data store called FColor
that is of type TColor. Users can set the FColor variable by
manipulating the Color property:
__property TColor Color={read=FColor, write=SetColor};
SetColor is a simple procedure that sets the value of FColor
and calls the Windows API call InvalidateRect:
void SetColor(TColor Color)
{ FColor = Color; InvalidateRect(Handle, NULL, True); }
Just to be sure this information makes sense to you, taking a paragraph or
two
to provide a refresher course on InvalidateRect is worthwhile.
Here's the declaration for the routine:
BOOL InvalidateRect(
HWND hWnd, // handle of window with changed update region
CONST RECT * lpRect, // address of rectangle coordinates
BOOL bErase // erase-background flag
);
InvalidateRect forces the window specified in the first parameter to
redraw itself completely if the third parameter is set to True. If the
third parameter is set to False, only the portions of the window that
you specifically repaint are changed. The middle parameter is a pointer to the
RECT structure that can be used to define the area that you want to
redraw. Compare this function with the native BCB function called Invalidate.
Calls to InvalidateRect naturally force calls to the
TColorClick->Paint method:
void __fastcall TColorClock::Paint()
{
Canvas->Brush->Color = FColor;
TMyClock::Paint();
}
Paint sets the brush associated with the window's device context to
the color specified by the user; then it calls the Paint method defined
in TClock.
To add this object to the Component Palette, you must first register it:
void __fastcall Register()
{
TComponentClass classes[2] = {__classid(TMyClock), __classid(TColorClock)};
RegisterComponents("Unleash", classes, 1);
... // Code omitted here
};
Here, I specify that the TClock and TColorClock objects
should be placed in a group called Unleash.
A number of properties implemented by TCustomControl and its
ancestors do not surface automatically. To access these properties from the
Component Palette, all you have to do is list them in the __published
section of your class declaration:
__property Align;
__property Color;
__property Font;
These simple declarations give your component the ability to align itself
with the surface on which it resides or set its color and font. The color, in
this case, is the color of the background of the form, not of the face of the
form. In the actual finished version of the control, I ended up replacing the
Color property with the TClockAttributes property. Once again,
the primary reason for adding the TClockAttributes property was to show
you how to work with property editors.
There is, of course, a place where the Align, Color, and
Font properties are declared replete with read and write parts.
However, after you have declared the property, you can publish it simply by
using the two-word declarations shown here.
On the CD that accompanies this book, you will find a program called
GoldClock that uses the TClock component to create a small
application displaying the time. You can customize the fonts and colors in this
program, and the settings you make will be written to the Registry so that they
can be preserved for the next run of the program. The main form of the GoldClock
application is shown in Figure 22.9.
Figure 22.9.The main form of the GoldClock application.
That's all I'm going to say about TClock and TColorClock.
Overall, these components are fairly simple; they're interesting primarily
because they show you how to go about constructing your own controls from
scratch. This kind of exercise lies very much at the heart of BCB's
architecture, and I expect many readers will be spending most of their time
engaged almost exclusively in the business of building components.
Tips on Creating Properties
A standard technique used in component development features base classes that
do not publish any properties. Consumers of the component can then inherit the
functionality of the class without having to stick with the original developer's
choice of published
properties.
After a property has been published, there is no good way to "unpublish" it.
The solution to this potential problem is to create base classes that do not
publish any properties. The functionality is present, but it is not surfaced in
a way that it can be viewed in the Object Inspector.
In many cases, the VCL creates classes that do nothing but publish the
properties of their immediate ancestors. These classes sport no functions, no
methods, nothing but a __published section full of declarations like
the ones shown in the preceding section for the Align, Color,
and Font properties. This architecture lets others inherit the
capability of the class while still providing a choice regarding what will be
published.
The naming convention used for classes of this type usually involves the word
Custom: TCustomImageList, TCustomControl,
TCustomMemoryStream,
TCustomGroupBox, TCustomLabel, TCustomEdit,
TCustomMemo, TCustomCheckBox, and so on. Open StdCtrls.hpp
to see more explicitly how this system works.
Making descendants from TEdit or TLabel as I do in this
chapter is a perhaps a bit too simplistic. If you really want to create a custom
version of the Edit control for your own purposes, you should probably make
descendants from TCustomEdit. Once again, you would not miss any
functionality by doing so because TEdit does nothing but surface the
"hidden" properties of TCustomEdit. It has no methods or data of its
own; everything is inherited from TCustomEdit. All it does is publish
20-odd properties.
You probably should not declare the read and write parts of a property in a
published section because doing so forces consumers of the product to conform
with your ideas for the component's interface. Instead, read and write
declarations should go in the public section of a class that has
Custom in the name. Then you should create a descendant of that class which
actually publishes the property.
Creating Icons for Components
The icon associated with a component and placed in the Component Palette is
defined in a file with a .DCR extension. If you do not provide this
file, BCB uses the icon associated with the object's parent. If no icon is
present anywhere in the component's ancestry, a default icon is used.
NOTE: A DCR file is a Windows resource
file with the extension changed from .RES to .DCR. The
resource file contains a bitmap resource with the same name as your component.
For example, the bitmap resource in a DCR file for a TCOLOR component
would have a resource ID of TColor. This resource should be a 56x28
pixel (or smaller) bitmap that can be edited in the Image Editor. All you need
to do is place this DCR file in the same directory as your component, and the
images defined therein will show up on the Component Palette. Use the Image
Editor to explore the DCR files that ship with BCB. They are stored in the
\CBUILDER\LIB directory. I also ship some DCR files with this book in the
Utils subdirectory where CodeBox and other files are kept.
Follow these steps to associate your own bitmaps with a particular component.
- 1. Open the Image Editor from the Tools menu and choose New.
2. Chose File | New | Component Resource from the menu. A dialog called
Untitled1.dcr appears.
3. Select Resource | New | Bitmap from the menu. A dialog called New
Bitmap Attributes appears.
4. Set Colors to 16 colors, because this technology is available on nearly
all Windows systems. Set Size in Pixels to 24x24.
5. Save the file as Clock1.dcr. Rename the bitmap you have
created to Tmyclock.
If you don't like the Image Editor, you can create a 24x24 bitmap in
Pbrush.exe or some other bitmap editor and then create an RC file that
looks like this:
TMYCLOCK BITMAP clock.bmp
Save the file as Clock1.rc. Run the Borland Resource Compiler from
the command line:
brcc32 -r clock.rc
The resulting file will be called Clock1.res. Rename that file
Clock1.dcr.
Creating Help Files for Components
BCB enables you to define help files that can ship with your components.
These help files can be merged into the help that ships with BCB, so users of
your product will feel as though it is a native part of BCB.
The HLP file is a standard Windows help file that contains information about
the properties, methods, and events implemented by your component. I strongly
recommend that you obtain a third-party tool to aid in the construction of these
help files. For example, you might buy a copy of FOREHELP from Borland, or you
could turn to Blue Sky software, or some other third party that provides tools
to help you create these files. As a last resort, you can use Word, WordPerfect,
or even the TRichEdit component, to create RTF files, and then compile
these RTF files into HLP files with the HCW.EXE help compiler that
ships with BCB in the CBUILDER\HELP\Tools directory. You can also
download this file from online sites such as the Microsoft Web site or
CompuServe. (The copy of FOREHELP sold by Borland has not been upgraded for
Windows 95, but it is still many times better than having no help tool at all.
FOREHELP requires that you have a copy of HC.EXE or HCP.EXE on
your system. These files ship with many compilers and are available royalty free
on CompuServe and the Internet.)
All the tools mentioned here play a peripheral role in the creation of
components. They don't require a knowledge of the fascinating syntax used in
component creation, but they help to make your tools attractive and easy to use.
The Five Main Tools APIs
Each of the five main Tools APIs is accessible through a separate set of
routines that ship with BCB. These APIs enable you to write code that can be
linked directly into the BCB IDE. Specifically, you can link your tools into
CmpLib32.ccl the same way you link in components. Here is a list of the
Tools APIs and the native BCB source files that define them.
Experts: Enable you to write your own experts.
- ExptIntf.hpp
- VirtIntf.hpp
- ToolIntf.hpp: for enumerating the component pages and components
installed, adding modules to the project, and so on
Version Control: Enables you to write your own Version Control system or to
link in a third-party system.
- VcsIntf.pas: Include this file in your project to generate the
HPP file. The Pascal source might not ship with all versions of BCB.
- VirtIntf.hpp.
- ToolIntf.hpp: for opening and closing files in the editor.
Component Editors: Create dialogs associated with a control at design time
(for example, the DataSet Designer is a component editor).
Property Editors: Create editors for use in the Object Inspector.
Editor Interfaces: For use mostly by third-party editor vendors such as
Premia, American Cybernetics, and so on.
- EditIntf.hpp
- FileIntf.hpp
The letters INTF are an abbreviation for the word "Interface." This
term was chosen because the Tools API is an interface between your own code and
the BCB developers' code.
Needless to say, most people will never use the Tools API. However, it will
be important to a small minority of developers, and its existence means that
everyone will be able to buy or download tools that extend the functionality of
the IDE.
Property Editors
The Tools API for creating property editors is perhaps the most commonly used
interface into the heart of the IDE. When you first use BCB and start becoming
familiar with the Object Inspector, you are bound to think that it is a static
element that never changes. However, by now it should come as no surprise to
learn that you can change the functionality of the Object Inspector by adding
new property editors to it.
As I mentioned earlier, property editors control what takes place on the
right side of the Properties page of the Object Inspector. In particular, when
you click the Color property of TEdit, you can select a new
color from a drop-down list, from a common dialog, or by typing in a new value.
In all three cases, you're using a property editor.
If you want to create a new property editor, you should create a descendant
of
TPropertyEditor, a class declared in DsgnIntf.hpp.
TPropertyEditor has a number of children. Many of these are for handling
simple types such as strings and integers. These editors are not very important
to BCB programmers, because most simple types have no RTTI associated with them
in C++.
Instead of working with simple types, BCB programmers work with properties
that are class types. The classic example of a class type of Property is
TFont. Note, for instance, that the Font property of a form can be expanded
to show the several fields of the TFont object. You can also pop up a
dialog to edit the TFont object. This is a complex property editor of
the type that will be developed through the remainder of this chapter.
Here is the declaration for the property editor associated with the
ClockAttributes property of the TMyClock component:
class TColorNameProperty: public TClassProperty
{
public:
TColorNameProperty(): TClassProperty() {}
TPropertyAttributes __fastcall GetAttributes(void);
};
TColorNameProperty doesn't descend from TPropertyEditor
directly. Instead, it descends from a child of TPropertyEditor called
TClassEditor. TClassEditor is designed for programmers who
want to create editors for complex properties that are also classes.
The TColorNameProperty editor doesn't pop up any dialogs, although
it will in a version of this program shown at the end of this chapter. Instead
of creating dialogs, it simply tells the Object Inspector that the
ClockAttributes class can be edited as two properties. In particular,
notice that the ClockAttributes class expands out to show a Color
property and a Shape property. You need to click the plus sign in front
of the ClockAttributes property before you can see these properties.
The GetAttributes method is a way of defining what types of property
editors you want to have associated with TColorDialog:
TPropertyAttributes __fastcall
TColorNameProperty::GetAttributes(void)
{
return TPropertyAttributes() << paSubProperties;
}
In this case I have set the paSubProperties flag, which tells the
Object Inspector to place the plus sign before the property so it can be opened
up to reveal sub properties.
A property editor that has the paMultiSelect flag remains active
even if the user has selected more than one component of that type. For example,
you can select 10 edit controls and change all their fonts in one step. BCB
enables you to make this change because the TEdit control has the
paMultiSelect flag set.
The paValueList flag dictates that the property editor drops down a
list of values from an enumerated or set type when the user clicks the arrow
button at the far right of the editor. This functionality is built into BCB, and
you need only set the flag to have it be supported by your property editor.
Finally, paDialog states that the property editor pops up a dialog.
Ultimately, the paDialog flag does little more than assure that the
ellipsis button appears at the right of the property editor.
NOTE: When you choose both paDialog
and paValuelist in a single component, the property editor button
always winds up being a combo drop-down list button. In other words, the
dialog button is obscured, even though the functionality is still present.
See, for example, the Color property of a TForm or TEdit.
Streaming Properties of Type Class
The ClockAttributes property will not work properly unless it is
streamed to disk. In particular, effective use of TClockAttributes
depends on whether the two key properties in TClockAttributes get
written to the DFM file where
the form is stored. To ensure that this happens, all you have to do is
declare ClockAttributes as a published property, and then descend
TClockAttributes from TPersistent, and make sure that the
Shape and Color properties are published.
Here is the declaration for the TClockAttributes:
class TClockAttributes: public TPersistent
{
private:
TColor FColor;
TShapeType FShape;
TComponent *FOwner;
void __fastcall SetColor(TColor AColor);
void __fastcall SetShape(TShapeType AShape);
public:
virtual __fastcall TClockAttributes() : TPersistent() { }
virtual __fastcall TClockAttributes(TComponent *AOwner)
: TPersistent() { FOwner = AOwner; }
__published:
__property TColor Color={read=FColor, write=SetColor};
__property TShapeType Shape={read=FShape, write=SetShape};
};
The key part of this declaration is where the Color and Shape
properties are designated as published. This means that they will have RTTI
associated with them, and that they will be streamed out to the DFM file
automatically.
Here is a text version of the TMyClock component as it appears in a
DFM file:
object MyClock2: TMyClock
Left = 112
Top = 192
Width = 100
Height = 100
ClockAttributes.Color = clBtnFace
ClockAttributes.Shape = stEllipse
end
Note that the two key properties of ClockAttributes have been
included in the DFM file. I did not have to do anything special to make this
happen. Instead, I just made sure that ClockAttributes itself was a
published property, and that Color and Shape were published
properties of TClockAttributes.
Once I was sure the properties were being streamed out, the only thing left
to do was tell the Object Inspector to show both values of TClockAttributes.
To do this, I simply created the simple, do-nothing TColorNameProperty
class. As you saw, this class descends from TClassProperty, which is
declared in Dsdintf.hpp.
Examining DsgnIntf.cpp
The DsgnIntf unit is unusual in that it is very carefully documented
by the developer who created it. For example, here are excerpts from that unit
describing the two methods I call in the descendant of TPropertyEditor:
Edit
Called when the `...' button is pressed or the
property is double-clicked. This can, for example,
bring up a dialog to allow the editing of the component
in some more meaningful fashion than by text
(e.g. the Font property).
GetAttributes
Returns the information for use in the Object
Inspector to be able to show the appropriate tools.
GetAttributes return a set of type TPropertyAttributes.
These entries were written by the developers, and they extensively document
this important interface to the core code inside the heart of the IDE.
Here are declarations for Edit and GetAttributes, as well
as the other key functions in TPropertyEditor:
class __declspec(pascalimplementation) TPropertyEditor
: public System::TObject
public:
__fastcall virtual ~TPropertyEditor(void);
virtual void __fastcall Activate(void);
virtual bool __fastcall AllEqual(void);
virtual void __fastcall Edit(void);
virtual TPropertyAttributes __fastcall GetAttributes(void);
Classes::TComponent* __fastcall GetComponent(int Index);
virtual int __fastcall GetEditLimit(void);
virtual System::AnsiString __fastcall GetName(void);
virtual void __fastcall GetProperties(TGetPropEditProc Proc);
Typinfo::PTypeInfo __fastcall GetPropType(void);
virtual System::AnsiString __fastcall GetValue(void);
virtual void __fastcall GetValues(Classes::TGetStrProc Proc);
virtual void __fastcall Initialize(void);
void __fastcall Revert(void);
virtual void __fastcall SetValue(const System::AnsiString Value);
bool __fastcall ValueAvailable(void);
}
Once again, all these methods are carefully documented in DsgnIntf.hpp.
You should study that file carefully in order to add to the knowledge about
property and component editors outlined in the remainder of this chapter.
Registering the Property Editors
You must register property editors with the system before compiling them into
CmpLib32.ccl. To register a property, call RegisterPropertyEditor
in the Register method for your component:
RegisterPropertyEditor(__typeinfo(TClockAttributes),
__classid(TMyClock), "Colors", __classid(TColorNameProperty));
The declaration for RegisterPropertyEditor looks like this:
extern void __fastcall RegisterPropertyEditor(
Typinfo::PTypeInfo PropertyType,
System::TMetaClass* ComponentClass,
const System::AnsiString PropertyName,
System::TMetaClass* EditorClass);
The various parameters are as follow:
- PropertyType. The first parameter passed to this function states
the type of data handled by the editor. In this case, it is
TClockAttributes. BCB uses this information as the first in a series of
checklists that determine which properties should be associated with this
editor. As noted above, this type cannot be a simple value such as an int
or TColor. It must be a class type. If you want to work with simple
types, you should either wrap them in a class or switch to Object Pascal.
- ComponentClass. The second parameter further qualifies which
components will use this editor. In this case, I have narrowed the range down
to TMyClock and its descendants. If I had written TComponent
instead of TMyClock, or if I had set this parameter to NULL,
all properties of type TClockAttributes would start using that
editor. You therefore can build a new editor for fonts or colors, install it
on a customer's system, and it would work with all properties of that type. In
other words, you don't have to create a component to be able to write an
editor for it.
- PropertyName. The third parameter limits the scope to properties
with the name passed in this string. If the string is empty, the editor is
used for all properties that get passed the first two parameters.
- EditorClass. This parameter defines the class of editor
associated with the properties defined in the first three parameters.
If you want to find out more about this function, refer to the comments in
DsgnIntf.pas. These comments may not be copied into DsgnIntf.hpp,
so check both places.
Working with Component Editors
After you have learned how to build property editors, understanding component
editors is easy. These tools are descendants of TComponentEditor, just
as property editors are descendants of TPropertyEditor:
class TClockEditor: public TComponentEditor
{
protected:
virtual __fastcall void Edit(void);
public:
virtual __fastcall TClockEditor(TComponent *AOwner, TFormDesigner *Designer)
: TComponentEditor(AOwner, Designer) {}
};
The TColorDialog has a simple editor that pops up a dialog
requesting that the user choose a new color:
void __fastcall TClockEditor::Edit(void)
{
TColorDialog *C = new TColorDialog(Application);
if (C->Execute())
((TColorClock *)(Component))->FaceColor = C->Color;
}
In this case, I'm popping up the standard TColorDialog that appears
when you click the ellipsis icon in the Object Inspector for a property of type
TColor. The difference, of course, is that this is a component editor,
not a property editor. In the more complex component editor example shown later
in this chapter, you pop open a form that allows the user to make several
changes to the component.
It is easy to access the underlying component from inside a component editor.
In particular, the component you are editing is available to you in a variable
called
Component.
The Component variable referenced in this example is declared
globally inside TComponentEditor objects. To access it, you merely have
to typecast it as desired to get at the component you're currently editing. For
example, in this example, I typecast Component as a TColorClock.
I don't bother with a dynamic_cast in this example because the
conversion has to work, by the very definition of the context of the call.
This component editor, of course, is the simplest possible, but it gets you
started working with these useful tools. Figure 22.10 shows the component editor
in action.
Figure 22.10.The first take on the component editor for the TClock
component pops up a simple TColorDialog.
NOTE: Two additional component editors,
each one more complex than the last, will be shown later in the chapter. In
particular, you will see how to pop up a component editor that is built on top
of a VCL form.
The Register method for TClockEditor looks like this:
void __fastcall Register()
{
. . .
RegisterComponentEditor(__classid(TMyClock), __classid(TClockEditor));
. . .
}
}
The declaration for this procedure looks like this:
extern void __fastcall RegisterComponentEditor(System::TMetaClass*
ComponentClass,
System::TMetaClass* ComponentEditor);
Clearly, the first parameter specifies the class with which the editor is
associated, and the second parameter specifies the class of the editor.
In this section, I introduced you to property editors and component editors.
These examples are important primarily because they help focus your attention on
DsgnIntf.hpp, which is one of several files that ship with BCB that
define the Tools API. If you want to extend the BCB IDE, you should get to know
all the files that end in INTF.
Extending the Component and Property Editors
The original component and property editors for TMyClock and TColorClock
components are very simple.
Of course, using such simple component and property editors is very limiting.
You may use a normal VCL form as an editor. Examples of this type of form are
shown in Figures 22.11 and 22.12.
Figure 22.11.The new property editor for ClockAttributes is a
VCL form that gives the user considerable control over the appearance of the
clock.
Figure 22.12.This component editor for TClock uses a standard
VCL form and can be as complex as you want.
To create the form shown in Figure 22.12, I started a new project called
TestClock2. I then copied the Clock1 files into a new set of files
called Clock2 and changed all the names inside the files to reflect the
change. For example, I renamed TMyClock to TMyClock2,
TColorClock to TColorClock2, and so on.
I then created a new form called ClockEditor and added several
buttons to it, a TColorDialog, and a TImage control. I
arranged the controls as shown in Figure 22.12. I then made a few changes to the
Edit method of TClockEditor2, and added a button to my main form so
that I could test the editor. The source code for this new program is shown in
Listings 22.12 through 22.15.
NOTE: Before installing the clock
components, you might need to run the BuildObjs program found in the Utils
directory. See the readme file on the CD that accompanies this chapter for
additional information. In particular, BuildObjs makes sure that
ClockAttributes.cpp and ClockEditor.cpp are both compiled into
binary form as OBJ files.
Listing 22.12. The header file for the main unit of
the TestClock2 program.
///////////////////////////////////////
// Main.h
// Project: Testing example of Component editor
// Copyright 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Clock2.h"
#include "Clock1.h"
class TForm1 : public TForm
{
__published:
TButton *Button1;
TColorClock2 *ColorClock21;
void __fastcall Button1Click(TObject *Sender);
private:
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 22.13. The main module for the TestClock2
program.
///////////////////////////////////////
// Main.cpp
// Project: Testing example of Component editor
// Copyright 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "ClockEditor.h"
#pragma link "Clock2"
#pragma link "Clock1"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
RunClockEditorDlg(ColorClock21);
}
Listing 22.14. The header for the ColorEditor dialog.
///////////////////////////////////////
// ClockEditor.h
// Project: Clock2 example of Component editor
// Copyright 1997 by Charlie Calvert
//
#ifndef ClockEditorH
#define ClockEditorH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Dialogs.hpp>
#include "Clock2.h"
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
void RunClockEditorDlg(TColorClock2 *Clock);
class TClockEditorDialog : public TForm
{
__published:
TButton *SetFaceColorBtn;
TColorDialog *ColorDialog1;
TButton *SetBackColorBtn;
TPanel *Panel1;
TImage *Image1;
TBitBtn *BitBtn1;
void __fastcall SetFaceColorBtnClick(TObject *Sender);
void __fastcall SetBackColorBtnClick(TObject *Sender);
private:
TColorClock2 *FClock;
public:
__fastcall TClockEditorDialog(TComponent* Owner);
__property TColorClock2 *Clock={read=FClock, write=FClock};
};
extern TClockEditorDialog *ClockEditorDialog;
#endif
Listing 22.15. The main module for the ColorEditor
dialog.
///////////////////////////////////////
// ClockEditor.cpp
// Project: Clock2 example of Component editor
// Copyright 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "ClockEditor.h"
#include "clock2.h"
#pragma resource "*.dfm"
TClockEditorDialog *ClockEditorDialog;
__fastcall TClockEditorDialog::TClockEditorDialog(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TClockEditorDialog::SetFaceColorBtnClick(TObject *Sender)
{
if (ColorDialog1->Execute())
{
Clock->FaceColor = ColorDialog1->Color;
}
}
void __fastcall TClockEditorDialog::SetBackColorBtnClick(TObject *Sender)
{
if (ColorDialog1->Execute())
{
Clock->ClockAttributes->Color = ColorDialog1->Color;
}
}
void RunClockEditorDlg(TColorClock2 *Clock)
{
ClockEditorDialog = new TClockEditorDialog(Application);
ClockEditorDialog->Clock = Clock;
ClockEditorDialog->ShowModal();
delete ClockEditorDialog;
}
Listing 22.16. The header for the VCL form that serves
a dialog for editing the ClockAttributes property.
///////////////////////////////////////
// ClockAttributes.h
// Clock Component with Component Editor
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef ClockAttributesH
#define ClockAttributesH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include "Clock2.h"
#include <vcl\Dialogs.hpp>
void RunClockAttributesDlg(TClockAttribute2 *AClockAttribute);
class TClockAttributesDialog : public TForm
{
__published:
TPanel *Panel1;
TComboBox *ComboBox1;
TBitBtn *GetColorsBtn;
TBitBtn *BitBtn2;
TColorDialog *ColorDialog1;
TImage *Image1;
void __fastcall GetColorsBtnClick(TObject *Sender);
void __fastcall BitBtn2Click(TObject *Sender);
private:
TClockAttribute2 *FClockAttribute;
public:
__fastcall TClockAttributesDialog(TComponent* Owner);
__property TClockAttribute2 *ClockAttribute =
{read=FClockAttribute, write=FClockAttribute};
};
extern TClockAttributesDialog *ClockAttributesDialog;
#endif
Listing 22.17. The main source file for the VCL form
that serves a dialog for editing the ClockAttributes property.
///////////////////////////////////////
// ClockAttributes.cpp
// Clock Component with Component Editor
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "ClockAttributes.h"
#pragma resource "*.dfm"
TClockAttributesDialog *ClockAttributesDialog;
__fastcall TClockAttributesDialog::TClockAttributesDialog(TComponent* Owner)
: TForm(Owner)
{
}
void RunClockAttributesDlg(TClockAttribute2 *AClockAttribute)
{
ClockAttributesDialog = new TClockAttributesDialog(Application);
ClockAttributesDialog->ClockAttribute = AClockAttribute;
ClockAttributesDialog->ComboBox1->ItemIndex = (int)AClockAttribute->Shape;
ClockAttributesDialog->ShowModal();
delete ClockAttributesDialog;
}
void __fastcall TClockAttributesDialog::GetColorsBtnClick(TObject *Sender)
{
if (ColorDialog1->Execute())
{
FClockAttribute->Color = ColorDialog1->Color;
}
}
void __fastcall TClockAttributesDialog::BitBtn2Click(TObject *Sender)
{
FClockAttribute->Shape = TShapeType(ComboBox1->ItemIndex);
}
Listing 22.18. Here is the header for the new version
of the clock components.
///////////////////////////////////////
// Clock2.h
// Clock Component
// Copyright (c) 1997 by Charlie Calvert
//
//------------------------------------
#ifndef Clock2H
#define Clock2H
#include <vcl\sysutils.hpp>
#include <vcl\controls.hpp>
#include <vcl\classes.hpp>
#include <vcl\forms.hpp>
#include <vcl\dsgnintf.hpp>
#include <vcl\messages.hpp>
class TClockAttribute2: public TPersistent
{
private:
TColor FColor;
TShapeType FShape;
TComponent *FOwner;
void __fastcall SetColor(TColor AColor);
void __fastcall SetShape(TShapeType AShape);
public:
virtual __fastcall TClockAttribute2(TComponent *AOwner)
: TPersistent() { FOwner = AOwner; }
__published:
__property TColor Color={read=FColor, write=SetColor};
__property TShapeType Shape={read=FShape, write=SetShape};
};
////////////////////////////////////////
// TMyClock ////////////////////////////
////////////////////////////////////////
class TMyClock2: public TCustomControl
{
private:
int FTimer;
Boolean FRunning;
TClockAttribute2 *FClockAttributes;
void __fastcall SetRunning(Boolean ARun);
protected:
virtual void __fastcall Paint(void);
void __fastcall WMTimer(TMessage &Message);
void __fastcall WMDestroy(TMessage &Message);
public:
virtual __fastcall TMyClock2(TComponent* Owner);
__published:
__property Boolean Running={read=FRunning, write=SetRunning};
__property Align;
__property TClockAttribute2 *ClockAttributes=
{read=FClockAttributes, write=FClockAttributes };
__property Font;
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_TIMER, TMessage, WMTimer);
MESSAGE_HANDLER(WM_DESTROY, TMessage, WMDestroy);
END_MESSAGE_MAP(TCustomControl);
};
////////////////////////////////////////
// TColorClock /////////////////////////
////////////////////////////////////////
class TColorClock2: public TMyClock2
{
private:
TColor FFaceColor;
void __fastcall SetColor(TColor Color);
protected:
void virtual __fastcall Paint(void);
public:
virtual __fastcall TColorClock2(TComponent *Owner)
:TMyClock2(Owner) { FFaceColor = clGreen; }
__published:
__property TColor FaceColor={read=FFaceColor, write=SetColor, nodefault};
};
////////////////////////////////////////
// TClockEditor ////////////////////////
////////////////////////////////////////
class TClockEditor2: public TComponentEditor
{
protected:
virtual __fastcall void Edit(void);
public:
virtual __fastcall TClockEditor2(TComponent *AOwner, TFormDesigner *Designer)
: TComponentEditor(AOwner, Designer) {}
};
////////////////////////////////////////
// TColorNameProperty /////////////////
////////////////////////////////////////
class TClockAttributeProperty: public TClassProperty
{
public:
TClockAttributeProperty(): TClassProperty() {}
TPropertyAttributes __fastcall GetAttributes(void);
void virtual __fastcall Edit(void);
};
#endif
Listing 22.19. The main source file for the new
version of the clock components.
///////////////////////////////////////
// Clock2.cpp
// Clock Component with Component Editor
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Clock2.h"
#include "ClockEditor.h"
#include "ClockAttributes.h"
#pragma link "ClockEditor.obj"
#pragma link "ClockAttributes.obj"
///////////////////////////////////////
// ValidControlCheck
///////////////////////////////////////
static inline TMyClock2 *ValidCtrCheck()
{
return new TMyClock2(NULL);
}
///////////////////////////////////////
// TClockAttributes
///////////////////////////////////////
void __fastcall TClockAttribute2::SetColor(TColor AColor)
{
FColor = AColor;
((TControl *)(FOwner))->Invalidate();
};
void __fastcall TClockAttribute2::SetShape(TShapeType AShape)
{
FShape = AShape;
((TControl *)(FOwner))->Invalidate();
};
///////////////////////////////////////
// Constructor
///////////////////////////////////////
__fastcall TMyClock2::TMyClock2(TComponent* Owner)
: TCustomControl(Owner)
{
Width = 100;
Height = 100;
FTimer = 1;
FClockAttributes = new TClockAttribute2(this);
FClockAttributes->Color = clBtnFace;
FClockAttributes->Shape = stEllipse;
}
///////////////////////////////////////
// SetRunning
///////////////////////////////////////
void __fastcall TMyClock2::SetRunning(Boolean Run)
{
if (Run)
{
SetTimer(Handle, FTimer, 50, NULL);
FRunning = True;
}
else
{
KillTimer(Handle, FTimer);
FRunning = False;
}
}
///////////////////////////////////////
// Paint
///////////////////////////////////////
void __fastcall TMyClock2::Paint(void)
{
Color = ClockAttributes->Color;
switch (ClockAttributes->Shape)
{
int X;
case stEllipse: Canvas->Ellipse(0, 0, Width, Height); break;
case stRectangle: Canvas->Rectangle(0, 0, Width, Height); break;
case stRoundRect:
Canvas->RoundRect(0, 0, Width, Height, Width - 100, Height);
break;
case stSquare:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Rectangle(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Rectangle((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
case stCircle:
{
if (Width < Height)
{
X = Width / 2;
Canvas->Ellipse(Width - X, 0, Width + X, Width);
}
else
{
X = Height / 2;
Canvas->Ellipse((Width / 2) - X, 0, (Width / 2) + X, Height);
}
break;
}
}
}
///////////////////////////////////////
// WM_TIMER
///////////////////////////////////////
void __fastcall TMyClock2::WMTimer(TMessage &Message)
{
AnsiString S = TimeToStr(Time());
Canvas->Font = Font;
Canvas->TextOut(Width / 2 - Canvas->TextWidth(S) / 2,
Height / 2 - Canvas->TextHeight(S) / 2, S);
}
///////////////////////////////////////
// WM_DESTROY
///////////////////////////////////////
void __fastcall TMyClock2::WMDestroy(TMessage &Message)
{
KillTimer(Handle, FTimer);
FTimer = 0;
TCustomControl::Dispatch(&Message);
}
//------------------------------------
//-- TColorClock2 --------------------
//------------------------------------
///////////////////////////////////////
// WM_DESTROY
///////////////////////////////////////
void __fastcall TColorClock2::Paint()
{
Canvas->Brush->Color = FFaceColor;
TMyClock2::Paint();
}
///////////////////////////////////////
// SetColor
///////////////////////////////////////
void __fastcall TColorClock2::SetColor(TColor Color)
{
FFaceColor = Color;
InvalidateRect(Handle, NULL, True);
}
//------------------------------------
//-- TClockEditor --------------------
//------------------------------------
void __fastcall TClockEditor2::Edit(void)
{
RunClockEditorDlg(((TColorClock2 *)(Component)));
}
//------------------------------------
//-- TColorNameProperty --------------
//------------------------------------
TPropertyAttributes __fastcall TClockAttributeProperty::GetAttributes(void)
{
return TPropertyAttributes() << paSubProperties << paDialog;
}
void __fastcall TClockAttributeProperty::Edit()
{
TClockAttribute2 *C = (TClockAttribute2 *)GetOrdValue();
RunClockAttributesDlg(C);
Modified();
}
///////////////////////////////////////
// Register
///////////////////////////////////////
namespace Clock2
{
void __fastcall Register()
{
TComponentClass classes[2] = {__classid(TMyClock2), __classid(TColorClock2)};
RegisterComponents("Unleash", classes, 1);
RegisterComponentEditor(__classid(TColorClock2), __classid(TClockEditor2));
RegisterPropertyEditor(__typeinfo(TClockAttribute2),
__classid(TMyClock2), "ClockAttributes",
__classid(TClockAttributeProperty));
}
}
When popping up a new form from inside a component editor, you have to create
the actual instance of the form and assign it an owner. You need to do so
because the form will not be auto-created in your program's project source file.
To handle the chore of creating the form, I created a special method inside
the ClockEditor unit. The routine looks like this:
void RunClockEditorDlg(TColorClock2 *Clock)
{
ClockEditorDialog = new TClockEditorDialog(Application);
ClockEditorDialog->Clock = Clock;
ClockEditorDialog->ShowModal();
delete ClockEditorDialog;
}
This routine takes an instance of the clock that needs to be edited as its
sole parameter. It then creates an instance of the TClockEditorDialog
and assigns the clock to an internal data store of the dialog. Finally, the code
shows the dialog and cleans up the memory when the user is through making the
edits.
To test this dialog, you can simply drop an instance of the clock onto the
main form of the application and then call the dialog from a button click
response method:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
RunClockEditorDlg(ColorClock21);
}
After you're sure that everything is working properly, you can change the
TClockEditor2::Edit method so that it calls this dialog when the user
double-clicks the component in design mode:
void __fastcall TClockEditor2::Edit(void)
{
RunClockEditorDlg(((TColorClock2 *)(Component)));
}
Once again, you can see that the code takes advantage of the Component
object that is declared ahead of time by the VCL. All you need to do is typecast
the object to the proper type and then pass it on to the ClockEditor
object.
Inside the TClockEditorDialog itself, the act of changing the
component is trivial:
void __fastcall
TClockEditorDialog::SetFaceColorBtnClick(TObject *Sender)
{
if (ColorDialog1->Execute())
{
Clock->FaceColor = ColorDialog1->Color;
}
}
This code simply pops up a color dialog and allows the user to choose a new
face color. If the user makes a selection, the color is assigned to the relevant
property of the TColorClock2 object.
I still need to address one subject in this project. The issue here is that
the TClock2 project now consists of two different modules. The first
module contains the TColorClock itself, and the second module contains
the TClockEditorDialog.
When you add the Clock2.cpp module to the Component Palette, you
also have to find a way to bring in ClockEditor.cpp; otherwise, the
project will not compile. Unfortunately, you can't add ClockEditor
directly to the CmpLib32 project because ClockEditor does not
have a Register method. Without the Register method, the
component library won't know what to do with the file.
The solution to this problem is provided by a pragma that can be
added to Clock2.cpp to tell the compiler that it must include
ClockEditor.obj in any projects that use this unit:
#pragma link "ClockEditor.obj"
After you add this line, CmpLib32 will know that it has to include
ClockEditor.obj in the compilation. It will not, however, know to
compile the unit from a CPP file into an OBJ file.
NOTE: In the first shipping version of
BCB, a problem can occur if you try to recompile CmpLib32.ccl and do
not provide a precompiled copy of ClockEditor.obj. In particular, the
compiler does not treat ClockEditor as part of the project it is
making, so it will not know to make ClockEditor.obj from
ClockEditor.cpp.
One solution to this problem is to start a small project that will compile
ClockEditor.cpp into ClockEditor.obj. For example, the simple
TestClock2 program will create ClockEditor.obj each time it
is compiled. The BuildObjs file included on the CD will do the same thing.
If you have created a copy of the OBJ file, and the compiler still complains,
try touching the unit that uses ClockEditor. For example, make a
small change to Clock2.cpp. Any change will do. You can just add a
space to the unit and then remove it. The point is that you need to save the
unit so that it has a new date and time. This time, when you recompile and
link CmpLib32, it will find your new copy of ClockEditor.obj.
That's all I'm going to say about component editors. As you have seen, this
subject
is not complicated, even when you create your own custom form for editing the
object. Creating custom forms of this type can be very helpful for the user
because it gives you a chance to step him or her through the process of
initializing the object. You can even create a little expert that runs when the
user double-clicks the component. The expert could take the user by the hand and
ensure that he or she sets up your control properly.
The property editor for the ClockAttributes property is very similar
to the component editor. The primary difference is in the way you access the
underlying value that you want to edit:
void __fastcall TClockAttributeProperty::Edit()
{
TClockAttribute2 *C = (TClockAttribute2 *)GetOrdValue();
RunClockAttributesDlg(C);
Modified();
}
Instead of having a ready-to-use variable called Component, you need
to call the GetOrdValue method of TPropertyEditor. This
function was originally intended to retrieve simply ordinal values. It can,
however, do double-duty and retrieve pointer values. This is possible because a
pointer and an integer both consist of 4 bytes.
In this case, I typecast the value returned from GetOrdValue as a
variable of type TClockAttribute. I can then pass this value into my
VCL form and edit it as I please. The principle here is exactly the same as in
the Component editor, only this time I access the underlying property that I
want to edit through GetOrdValue, rather than through a variable named
Component.
Inside the ClockAttributes dialog you need to create a routine that will pop
open the VCL form:
void RunClockAttributesDlg(TClockAttribute2
*AClockAttribute)
{
ClockAttributesDialog = new TClockAttributesDialog(Application);
ClockAttributesDialog->ClockAttribute = AClockAttribute;
ClockAttributesDialog->ComboBox1->ItemIndex = (int)AClockAttribute->Shape;
ClockAttributesDialog->ShowModal();
delete ClockAttributesDialog;
}
Most of this code is very routine and does nothing more than allocate memory
for a form, show it modally, and then destroy it. Note the line that sets the
value of the selected item in the form's combo box:
ClockAttributesDialog->ComboBox1->ItemIndex =
(int)AClockAttribute->Shape;
This line accesses one of the fields of the TClockAttribute class.
It then sets the ItemIndex of the combo box to the currently selected
shape. Here are the values displayed in the combo box:
Rectangle
Square
RoundRect
RoundSquare
Ellipse
Circle
If the technique used here to select an item in the combo box does not make
sense to you, you might want to refer to similar code from the ShapeDem2
program in Chapter 2.
Summary
In this chapter, you have learned about building components. Specifically,
you learned how to create components that do the following:
- Change an ancestor's default settings. For example, you created TEdit
descendants with new default colors and fonts.
- Add functionality not available in an ancestor object. For example, the
TColorClock object does things that TClock does not.
- Are built up from scratch so that they can add new functionality to the
IDE. For example, the TClock component brings something entirely new
to BCB programming that does not exist in any other component that ships with
the product.
You also learned about tools and files associated with components, such as
the DCR and KWF files. Finally, you learned about the Tools API and specifically
about the art of making property editors and component editors.
|