| In this chapter you will get a look at the interface between
BCB and the Object Pascal code found in the VCL. This is a subject you need to
understand if you want to take full advantage of the power of Borland C++Builder.
There are very few occasions when BCB programmers have to think explicitly about
the fact that the VCL is written in Object Pascal. Most of the time you can
forget its Delphi-based heritage without fear of missing out on anything
important. There are, however, a few times when Object Pascal affects your
programming. Almost all my descriptions of those occasions are concentrated in
this chapter. Throughout most of the rest of this book the subject will never
even arise.
The material in this chapter is divided into three main sections:
- 1. Understanding the VCL
2. Changes to the C++ Language
3. Classes created to imitate Object Pascal simple types that do not exist
in C++
Understanding the VCL
All VCL objects are referenced as pointers. There is no such thing as a
static or local instance of a VCL object. You are always working with a pointer
to a VCL object. This is the result of the VCL's origin in the world of Object
Pascal.
Delphi sought to simplify its syntax to the greatest degree possible.
However, there were several hurdles that had to be crossed before the language
could be made accessible to a wide variety of programmers. In particular,
something had to be done about the complexity of dealing with pointers.
For various reasons related to performance and wise use of memory, it is
usually best to declare an object on the heap, rather than on the stack,
particularly if you are still inhabiting the segmented world of 16-bit
programming. As a result, the designers of Object Pascal wanted to make it as
simple as possible for their users to work with objects that live on the heap.
Rather than inflict pointer syntax on unwary Object Pascal programmers, the
creators of the VCL decided that all objects would necessarily be created on the
heap, but would support the syntax associated with local objects. In short, they
created a world that eliminated pointer syntax for objects altogether. They
could afford to do this because they made it impossible to create an object that
was not a pointer. All other language types, such as strings, integers, arrays,
structures, and floating-point numbers, could be treated either as pointers or
as static objects. This rule applied only to objects.
It might help to illustrate this point with examples. Here is hypothetical
code for how an Object Pascal programmer might have treated objects according to
the standard rules of Pascal:
var
S: ^TObject;
begin
S := New(Tobject, Create);
S^.DoSomething;
S^.Free;
end;
The preceding code will not compile in Delphi, but it is an example of how
Delphi code might have looked had the developers not done something to simplify
matters. In particular, Delphi eliminated some syntactical clutter by enabling
you to write the following:
var
S: TObject;
begin
S := TObject.Create;
S.DoSomething;
S.Free;
end;
Clearly this is an improvement over the first example. However, both samples
produce essentially the same underlying machine code. In other words, both
examples allocate memory for the object, call a constructor called Create,
implicitly call a method called DoSomething, call a destructor called
Destroy, and then Free the memory associated with the object.
In the second example, all of this can be done without any need to dereference a
pointer with the "^" symbol or without making explicit reference to the
act of allocating memory. The point being that the compiler knows that the
variable S has to be a pointer to an object, because Object Pascal
forbids the creation of objects that are not pointers.
Clearly, in the Object Pascal world, it made sense to decide that all objects
had to be pointers. There is no significant overhead involved with using
pointers, and indeed they are often the fastest way to manipulate memory. So why
not make this one hard and fast rule in order to make everyone's life simpler?
Translate this same concept into C++, and suddenly the rule doesn't make
quite as much sense. In particular, you can only dare go so far when changing
the C++ language to accommodate a new paradigm, and you therefore can't reap any
of the benefits that adhered to the Object Pascal code shown in the second of
the two most recently listed code samples. In other words, C++ reaps no benefits
from this rule, while it does tend to limit your choices. In other words, you
are no longer free to create VCL objects locally, but must perforce create them
on the heap, whether you want to or not.
I will therefore take it as a given that the need to create VCL objects on
the heap is not particularly beneficial to C++ programmers. On the other hand,
there is no particular hardship inherent in doing so, nor does it force you to
endure any hit on the performance of your application. It is therefore merely a
fact of life, neither inherently good nor inherently bad. If you find this
unduly irksome, you might consider the many other benefits that BCB brings you,
and consider this one
limitation as a small price to pay for getting the benefit of this product's
strengths.
One final, and not at all unimportant, point: Only VCL objects have to be
created on the heap. All standard C++ objects can be handled as you see fit. In
other words, you can have both static or dynamic instances of all standard C++
objects. It's only VCL objects that must be addressed with pointers and must be
allocated on the heap!
Changes to the C++ Language
It's now time to wrap up the introductory portion of this chapter, which
provided an overview of the VCL and BCB programming. The next subject on the
agenda involves how the VCL has impacted the C++ language and how the C++
language has affected the VCL. It's perhaps simplest to start with a run down of
the new features in the C++ language.
I am, of course, aware of how controversial it is to add extensions to C++.
However, I personally am only interested in good technology and the quality of
the products I use. BCB is a good product, and part of what makes it good is the
power of the VCL and the power of the component, property, event model of
programming. The new extensions have been added to the language to make this
kind of programming possible, and so I am in favor of these changes. The C++
committee has done excellent work over the years, but no committee can keep up
with the furious pace of change in this industry.
The following extensions have been added to the language in order to support
the VCL and the component, property, and delegation model of programming:
_ _declspec(delphiclass | delphireturn)
_ _automated
_ _published
_ _closure
_ _property
_ _classid(class)
_ _fastcall
As you can see, all of these keywords have two underscores prefixed to them.
In this instance, I have separated the underscores with a single space to
emphasize the fact that not one, but two underscores are needed. In your
programs, the underscores should be contiguous, with no space between them.
I am going to go through all these new keywords and make sure that you
understand what each means. The purpose of each of these sections is not to
explain the how,
when, where, and why of a particular keyword, but only to give you a sense of
what you can and can't do with them. I will also say something about the
relative importance of each new piece of syntax. My goal is to create a handy
reference for the new keywords, but I will wait until later to explain most of
them in real depth. For instance, a lengthy discussion of the __automated
keyword appears in Chapter 27, "Distributed COM," which covers DCOM and
Automation, and I discuss properties at length in Chapters 19 through 24, which
cover creating components.
Automated
The automated keyword is for use in OLE automation classes. Only classes that
descend from TAutoObject or a descendant of TAutoObject would
have a use for this keyword.
Here is an example of a class that sports an automated section:
class TMyAutoObject : public TAutoObject
{
private:
public:
virtual __fastcall TMyAutoObject();
__automated:
AnsiString __fastcall GetName(void) { return "MyAutoObject"; }
};
If this class were compiled into a working program, the GetName
function would be available to other programs through OLE automation. They can
call the function and it will return the word "MyAutoObject".
The reason the GetName function can be accessed through OLE
automation is because it appears in the automated section of an object. Notice
that it is also declared __fastcall. This is necessary with automated
functions. It means the compiler will attempt to pass the parameters in
registers rather than on the stack.
The __automated directive makes OLE automation programming much
easier, but it's effect is limited to this one area. It has no far reaching
effect on the whole of the C++ language.
The Published Keyword
In addition to public, protected, and private, C++ now supports two new
keywords used to delineate a section in a class declaration where properties
that are to appear in the Object Inspector can be declared. If you are creating
a component with new properties that you want to appear in the Object Inspector,
you should place those properties in the published section. Properties that
appear in the published section have very complete RTTI generated for them by
the compiler.
It is only legal to use the __published keyword in VCL classes.
Furthermore, though the compiler may or may not enforce the rule, it is
generally not sensible to use the __published keyword in objects that
do not descend from TComponent or from a descendant of TComponent.
The reasoning here is simply that TComponent is a necessary ancestor of
all components, and the __published keyword exists because it helps to
enhance components. The RTTI associated with the __published section
will have some effect on program performance, so you don't want to use it unless
it will be helpful. Therefore, use __published only in classes that you
want to make into components.
Here is an example of an object that features a published section:
class TMyObject : public TComponent
{
private:
int FSomeInteger;
protected:
int AnotherInteger;
public:
int PublicInteger;
__published:
__property int SomeInteger={read=FSomeInteger, write=FSomeInteger};
};
In this declaration, the property called SomeInteger is published,
will be illuminated with RTTI, and will appear in the Object Inspector if you
install
TMyObject as a component.
The __published directive changes the way I think about classes, and
it changes the way I write code. It currently has significance only for VCL
classes and for your components. At this time it has no far-reaching
implications in regard to the structure of the C++ language. It is just
something that aids in the construction of VCL components.
Properties
Properties represent one of the most important, and most beneficial additions
to C++ that you will find in BCB. There will be several places in this book
where I discuss properties in depth. In this section I simply give you a general
outline of the key points regarding this subject.
Properties have four primary purposes:
- 1. They support a technology that allows you to expose part of an
object to visual programmers via the Object Inspector.
2. They provide an excellent means of creating a rigidly defined, and
thoroughly correct, interface for an object.
3. They make it easy for you to protect the private data of an object, and
the private portions of your implementation.
4. They support a very sophisticated form of RTTI which allows them to be
both seen in the object inspector, and to be automatically streamed out to and
read from disk without intervention from the programmer. The automatic
streaming of objects is one of the great features of the VCL.
Properties can be added to either the public or published
section of a class declaration. It makes no sense to put them in either the
protected or private sections, because their primary purpose is to
provide a public interface to your object.
Here is a declaration for a property:
class TMyObject : public TComponent
{
private:
int FSomeInteger;
__published:
__property int SomeInteger={read=FSomeInteger, write=FSomeInteger};
};
In this class, SomeInteger is a property of type int. It
serves as the public interface for the private FSomeInteger variable.
To declare a property:
- Use the __property keyword.
- Declare the type of the property.
- Add the name of the property.
- Add an equal sign and an open and close curly brace followed by a
semicolon.
- Between the curly braces, properties can have read and write sections.
These sections are used to designate how to set or get the value of a
property. To declare the read section, write the word read followed by an
equal sign, and do the same thing with the write section.
It is very common in VCL classes to declare private data with the
letter F prefixed to it. The letter F stands for field. It
serves as a reminder that this is a private variable and should not be
accessed from another class.
You can declare a property to be read only or write only:
__property int SomeInteger={read=FSomeInteger};
__property int SomeInteger={write=FSomeInteger};
The first of these declarations is read only, the second is write only.
Because FSomeInteger is private, you might need to provide some
means of accessing it from another object. You can and should sometimes use the
friend notation, but that really violates the whole concept of the
private directive. If you want to give someone access to your data, but
continue to hide the specific implementation of that data, use properties. The
preceding code provides one means of giving someone access to your data while
still protecting it. It goes without saying that if your property does nothing
else but call directly to a variable of the same type in the private
section, the compiler generates code that gives you direct access to the type.
In other words, it doesn't take any longer to directly use a property like
SomeInteger than it would to use FSomeInteger directly. The
compiler takes care of the details for you, and uses less code than it would
with an inline access function.
NOTE: C++ programmers have long used
get and set methods to provide access to private data. There is a
certain amount of controversy as to whether or not properties add something to
the object model outside of their capability to be seen in the Object
Inspector.
Here is a second way to state the matter. Given the presence of the Object
Inspector, properties clearly play an important role in BCB programming. A
second debate, however, would involve the question of whether or not--absent
the issue of the Object Inspector--properties add something new to the mix
that would not be there if we had only access functions and get and set
methods.
One thing I like a great deal about properties is the clarity of the syntax
they present to the user. They make it easier to write clear, maintainable
code. They also do a very good job of helping you protect the low-level
implementation of your object. I recognize, however, that it could be argued
that access functions provided sufficient power to accomplish these goals
without the aid of properties.
I could safely hide behind the fact that the BCB programming model demands the
presence of properties. However, I will stick my neck out on this one and say
that I feel properties are an important contribution to C++, and should become
part of the language. I recognize, however, that this is the kind of subject
that fosters intense controversy, and concede that the debate is not entirely
one-sided.
Here is another use of properties that differs from the one just shown:
class MyObject : public TObject
{
private:
int FSomeInteger;
int __fastcall GetSomeInteger() { return FSomeInteger; }
void __fastcall SetSomeInteger(int i) { FSomeInteger = i; }
__published:
__property int SomeInteger={read=GetSomeInteger, write=SetSomeInteger};
};
In this class, SomeInteger has get and set methods. These get and
set methods are associated with a property and should always be declared with
the __fastcall calling convention.
Get and set methods provide a means of performing calculations or other
actions when getting and setting the value of a property. For instance, when you
change the Width property in a TShape object, not only does
some internal variable get set, but the whole object redraws itself. This is
possible because there is a set method for this property that both sets the
internal FWidth property to a new value and redraws the object to
reflect these changes.
You could perform calculations from inside a get method. For instance, you
could calculate the current time in a property designed to return the current
time.
The existence of get and set methods represents another important reason for
keeping the data of your object private, and for not giving anyone else
access to it except through properties. In particular, you may change a value in
a set or get method or have side effects that you want to be sure are executed.
If someone accesses the data directly, the side effects or other changes will
not occur. Therefore, you should keep the data private and let them
access it through a function, and let the function itself be accessed through a
property.
Properties can be of a wide variety of types. For instance, they can be
declared as AnsiStrings, arrays, sets, objects, or enumerated types.
Events, which are a kind of method pointer, are really a type of property, but
they behave according to their own peculiar rules and are usually treated as a
separate subject from properties.
Properties can be declared to have a default value:
__property int SomeInteger={read=GetSomeInteger,
write=SetSomeInteger, default=1};
This syntax is related only tangentially to the concept of setting a property
automatically to a particular value. If you want to give a property a predefined
value, you should do so in the object's constructor.
Default is used to tell the VCL whether or not a value should be written out
to a stream. The issue here is that streaming can result in producing large
files, because so many objects have such large numbers of properties. To cut
down on the size of your form files, and on the time spent writing the files to
disk, the VCL enables you to declare a default value for a property. When it
comes time to stream properties to disk, they will not be streamed if they are
currently set to the default value. The assumption is that you will initialize
them to that value in the object's constructor, so there is no need to save the
value to disk. Many properties are declared to be nodefault, which means they
should be streamed to disk.
A property can also have the stored directive. Confusingly enough, this is
another means of deciding whether or not a property should be streamed. In this
case, it gives you the option of changing whether or not a property can be
streamed. For instance:
property TColor Color={read Fcolor, write SetColor,
stored=IsColorStored, default=clWindow};
This property calls a method named IsColorStored to determine
whether the color should be stored at this time. For instance, if the property
is set to have the same value as its parent, there is no need to store it, and
IsColorStored returns False. This property will therefore only
be stored if IsColorStored returns True and the color is not
set to clWindow.
Say what?
Don't worry, most of the time you don't have to get involved with these
tricky storage specifiers and their disarming schemes to save disk space.
However, there are times when the matter comes to the front, and now you have a
place to look to remember what they mean.
Properties play a big role in VCL programming, and furthermore, they can be
used in objects that have nothing to do with the VCL. This subject has
tremendous potential scope and will affect the way the C++ language as a whole
is handled by BCB programmers.
That is all I'm going to say about properties for now. There is an example of
creating and using an array property later in this chapter in the section called
"Arrays of AnsiStrings."
Events: Understanding Delegation
Most of the time I will refer to closures as events. Technically, a closure
is
the type of method pointer used in event properties, but I will generally
refer to the whole syntactical structure as an event that supports the
delegation model.
As you learned in the last section, an event is really a kind of property.
The
primary difference between a standard property and an event is that the type
of an event is a method pointer.
NOTE: Some people refer to events as
closures, but it could be argued that the word has a rather stuffy, academic
overtone to it. Certainly it is used in some circles in a very strict manner
that does not necessarily conform in all its particulars to the way BCB
handles events.
Consider the OnMouseUp event that is supported by many components
and declared in CONTROLS.HPP:
__property TMouseEvent OnMouseUp = {read=FOnMouseUp,
write=FOnMouseUp};
Like the OnMouseUp property, FOnMouseUp is, naturally, also
declared to be of type TMouseEvent:
TMouseEvent FOnMouseUp.
So far the syntax used for events seems pretty much identical to that used
for all properties. The new code that is specific to the delegation model is the
actual declaration for the method pointer used by an event:
typedef void __fastcall (__closure
*TMouseEvent)(System::TObject* Sender,
TMouseButton Button, Classes::TShiftState Shift, int X, int Y);
All OnMouseUp events are of this type. In other words, if an
OnMouseUp event is not set to null, it is set to a method pointer
of this type. You can then call the event by writing code of this type:
if (FOnMouseUp)
FOnMouseUp(this, Button, Shift, X, Y);
If the event is not set to null, call the method associated with the
event and pass parameters of a type that conform with the signature of the
relevant method pointer. This is called delegating the event.
NOTE: It is generally considered very
bad form to return a value from an event. Trying to do so could cause a
compiler error, and you should avoid including code of this type in your own
programs. The VCL has gone through several iterations now, and returning
values from events has worked in some versions and not in others. The team
that wrote the VCL, however, asked the DOC team to state explicitly that
events should not return values. It usually turns out badly when you get stuck
with legacy code that worked in one version, but directly contradicts the
desires of the folks who keep the reins in their own hands.
The method associated with the OnMouseUp event will look something
like this:
void __fastcall TForm1::Button1MouseUp(
TObject *Sender,
TMouseButton Button,
TShiftState Shift,
int X,
int Y)
{
}
I generally call a method of this type an event handler. It handles an event
when it is delegated by a component or object.
Most of the time an event is assigned to a method automatically by the
compiler. However, you can do so explicitly if you desire, and in fact I do this
in several places in my own code. If you want an example, see the Music program
from Chapter 16, "Advanced InterBase Concepts." Here is how the assignment would
be made from inside the Controls unit:
FOnMouseEvent = Button1MouseUp;
Code of this type appears everywhere in the VCL, and indeed it is one of the
central constructs that drives BCB. This is the heart of delegation programming
model. As I have said several times, BCB is primarily about components,
properties, and the delegation model. Take events away, and you have some other
product altogether. Events radically change the way C++ programs are written.
The key effect of events on C++ seems to me to be the following: "In addition to
using inheritance and virtual methods to change the behavior of an object, BCB
allows you to customize object by delegating events. In particular, VCL objects
often delegate events to the form on which they reside."
Like all properties, events work with both standard C++ classes and with VCL
classes.
declspec(delphiclass | delphireturn |
pascalimplementation)
All the additions to C++ discussed so far are primarily about finding ways to
support the VCL. In particular, the new VCL object model has certain benefits
associated with it, and so Borland has extended C++ in order to accommodate this
new model. The property and event syntax, however, appears to have at least some
other implications beyond its simple utilitarian capability to support the VCL.
I think you will find the next three changes are very small potatoes compared to
properties and events. All they really do is make it possible for BCB to use the
VCL, and they don't really have much effect at all on the way we write code.
As you have no doubt gleaned by this time, VCL classes behave in specific
ways. For instance, a VCL class can only by instantiated on the heap, and you
can only use the __published or __automated keywords with VCL
classes.
The majority of the time your own classes can inherit this behavior directly,
simply by descending from a VCL class. For instance, here is part of the
declaration for TControl
class __declspec(pascalimplementation) TControl :
public Classes::TComponent
{
typedef Classes::TComponent inherited;
private:
TWinControl* FParent;
... // etc
}
As you can see, these classes are declared with the delphiclass or
pascalimplementation
attribute. This means they are implemented in Pascal, and the headers are the
only part that appear in C++ code. If you have an Object Pascal unit that you
want to use in a BCB application, the other declarations in the unit will
automatically be
translated and placed in a C++ header file. The class declarations in that
header file will be similar to the one shown here.
However, if you create a descendant of TControl, you don't have to
bother with any of this, because the attributes will be inherited:
class MyControl : public TControl
{
// My stuff here
}
Class MyControl is automatically a VCL class that can support __published
properties and must be created on the heap. It inherits this capability from
TControl. As a result, it is implicitly declared pascalimpementation,
even though you don't explicitly use the syntax. I don't like to clutter up
class declarations with __declspec(pascalimplementation), but it may
appeal to some programmers because it makes it clear that the class is a VCL
class and has to be treated in a particular manner.
If you want to create a forward declaration for a class with the
pascalimplementation attribute, it must have the delphiclass
attribute:
class __declspec(delphiclass) TControl;
This is a forward declaration for the TControl class found in
CONTROLS.HPP. You do not inherit delphiclass in the same sense
that you do pascalimplementation, and so you must use it explicitly in
forward declarations!
delphireturn is used to tell the compiler to generate a particular
kind of code compatible with the VCL when it returns objects or structures from
a function. The only place in BCB where this is done is in SYSDEFS.H
and DSTRINGS.H, which is where classes like Currency,
AnsiString, and Variant are declared. If you need to return
objects or structures to the VCL, you may need to declare your class with this
directive. However, there are very few occasions when this is necessary, and
most programmers can forget about delphireturn altogether.
NOTE: As shown later in this chapter,
the TDateTime object is declared delphireturn. This is
necessary because instances of the object can be passed to VCL functions such
as DateTimeToString.
All of this business about dephireturn and pascalimplementation
has little or no effect on the code written by most BCB programmers.
delphiclass is slightly more important, because you will likely have
occasion to declare a forward declaration for one of your own descendants of a
VCL class. This is clearly a case where you can indeed afford to "pay no
attention to the man behind the curtain." Most of this stuff is just hand
signals passed back and forth between the compiler and VCL, and you can afford
to ignore it. Its impact on the C++ programming as a whole is essentially nil.
classid(class)
Use classid if you need to pass the specific type of a class to a
function or method used by the VCL RTTI routines. For instance, if you want to
call the ClassInfo method of TObject, you need to pass in the
type of the class you want to learn about. In Object Pascal you write
ClassInfo(TForm);
which means: Tell me about the TForm class. Unfortunately, the
compiler won't accept this syntax. The correct way to pose the question in BCB
is
ClassInfo(__classid(TForm));
Again, this is just the compiler and the VCL having a quiet little chat. Pay
no attention to the man behind the curtain. RTTI looks one way in the VCL,
another in standard C++. This syntax is used to bridge the gap between the two,
and it has no far reaching implications for C++, the VCL, or anyone else. The
excitement is all centered around the property and event syntax; this is a mere
trifle.
__fastcall
The default calling convention in Object Pascal is called fastcall,
and it is duplicated in C++Builder with the __fastcall keyword.
__fastcall methods or functions usually have their parameters passed in
registers, rather than on the stack. For instance, the value of one parameter
might be inserted in a register such as EAX and then snagged from that
register on the other side.
The __fastcall calling conventions pass the first three parameters
in eax, edx, and ecx registers, respectively.
Additional parameters and parameter data larger than 32 bits (such as doubles
passed by value) are pushed on the stack.
Delphi-Specific Classes
Not all the features of C++ are available in Object Pascal, and not all the
features of Object Pascal are available in BCB. As a result, the following
classes are created to represent features of the Object Pascal language that are
not present in C++:
AnsiString
Variant
ShortString
Currency
TDateTime
Set
Most of these classes are implemented or declared in SYSDEFS.H,
though the crucial AnsiString class is declared in its own file called
DSTRINGS.H.
NOTE: Neither the real nor
comp types used in Object Pascal are supported adequately in BCB. Because
Pascal programmers often used the comp type to handle currency, they
should pay special attention to the section in this chapter on the BCB
currency type. The real type is an artifact of the old segmented architecture
days when anything and everything was being done to save space and clock
cycles. It no longer plays a role in contemporary Object Pascal programming.
I will dedicate at least one section of this chapter to each of the types
listed previously. Some important types, such as Sets and
AnsiStrings, will receive rather lengthy treatment stretching over several
sections.
Many C++ programmers will have questions about the presence of some of these
classes. For instance, C++ comes equipped with a great set template and an
excellent string class. Why does BCB introduce replacements for these classes?
The answer to the question is simply that BCB needed a class that mimicked the
specific behavior of Object Pascal sets and Object Pascal strings.
Here are some macros you can use with these classes:
OPENARRAY
ARRAYOFCONST
EXISTINGARRAY
SLICE
These macros are also discussed in the upcoming sections.
Introducing the AnsiString Class
The C language is famous for its null-terminated strings. These strings can
be declared in many ways, but they usually look like one of these three
examples:
char MyString[100]; // declare a string with 100
characters in it.
char *MyString; // A pointer to a string with no memory allocated for it.
LPSTR MyString; // A "portable" Windows declaration for a pointer to a string
Declarations like these are such a deep-rooted and long standing part of the
C language that it is somewhat shocking to note that they are not as common as
they once were. There is, of course, nothing wrong with these strings, but C++
has come up with a new, and perhaps better, way to deal with strings. (I speak
of these strings as being exclusive to C, though of course these same type of
strings are also used in Object Pascal, where they are called PChars.)
The "old-fashioned" types of strings shown previously cause people problems
because it is easy to make memory allocation errors with them or to accidentally
omit the crucial terminating zero (`/0') that marks the end of these
strings.
These same strings are also famous for their accompanying string library,
which includes a series of cryptic looking functions with names like strcpy,
strlen, strcmp, strpbrk, strrchr, and so on.
These functions, most of which occur in both C++ and Object Pascal, can be
awkward to use at times, and can frequently lead to errors if a programmer gets
careless.
In this book, I will try to avoid using any of the "old fashioned" C style
strings whenever possible. Instead, I will do things the C++ way and use string
classes. Most C++ programmers prefer string classes on the grounds that they are
easier to use, easier to read, and much less likely to lead to an error
involving memory allocation.
In this book there is a fourth reason for using string classes. In
particular, a string class called AnsiString provides compatibility
with the underlying strings found in the VCL. C++ programmers will find that
AnsiStrings are similar to the standard ANSI C++ String class.
AnsiStrings are very easy to use. In fact, many programmers will
find that they have an intuitive logic to them that almost eliminates the need
for any kind of in-depth explanation. However, AnsiStrings happen to
form one of the key building blocks on which a great deal of C++Builder code is
based. It is therefore of paramount importance that users of BCB understand
AnsiStrings.
The next few sections of the book take on the task of explaining
AnsiStrings. To help illustrate this explanation with ready-made examples,
you can turn to the UsingAnsiString program found on the book's CD-ROM. This
program provides examples of essential AnsiString syntax and shows
several tricks you can use when you add strings to your programs.
Working with the AnsiString Class
The AnsiString class is declared in the DSTRING.H unit from
the ..\INCLUDE\VCL directory. This is a key piece of code, and one
which all BCB programmers should take at least a few minutes to study.
The declaration for the class in the DSTRING.H unit is broken up
into several discreet sections. This technique makes the code easy to read. For
instance, one of the sections shows the operators used for assignments:
// Assignments
AnsiString& __fastcall operator =(const AnsiString& rhs);
AnsiString& __fastcall operator +=(const AnsiString& rhs);
Another section shows some comparison operators:
//Comparisons
bool __fastcall operator ==(const AnsiString& rhs) const;
bool __fastcall operator !=(const AnsiString& rhs) const;
bool __fastcall operator <(const AnsiString& rhs) const;
bool __fastcall operator >(const AnsiString& rhs) const;
bool __fastcall operator <=(const AnsiString& rhs) const;
bool __fastcall operator >=(const AnsiString& rhs) const;
Another handles the Unicode-related chores:
//Convert to Unicode
int __fastcall WideCharBufSize() const;
wchar_t* __fastcall WideChar(wchar_t* dest, int destSize) const;
There is, of course, much more to the declaration than what I show here.
However, these code fragments should give you some sense of what you will find
in DSTRING.H, and of how to start browsing through the code to find
AnsiString methods or operators that you want to use.
The rest of the text in this section of the chapter examines most of the key
parts of the AnsiString class and shows how to use them. However, you
should definitely find time, either now or later, to open up DSTRING.H
and to browse through its contents.
AnsiString Class Constructors
AnsiStrings are a class; they are not a simple type like the string
type in Object Pascal, which they mimic. Object Pascal or BASIC programmers
might find that the next line of code looks a little like a simple type
declaration. It is not. Instead, this code calls the constructor for an object:
AnsiString S;
Here are the available constructors for the AnsiString class:
__fastcall AnsiString(): Data(0) {}
__fastcall AnsiString(const char* src);
__fastcall AnsiString(const AnsiString& src);
__fastcall AnsiString(const char* src, unsigned char len);
__fastcall AnsiString(const wchar_t* src);
__fastcall AnsiString(char src);
__fastcall AnsiString(int src);
__fastcall AnsiString(double src);
The simple AnsiString declaration shown at the beginning of this
section would call the first constructor shown previously, which initializes to
zero a private variable of the AnsiString class. This private variable,
named Data, is of type char *. Data is the core C
string around which the AnsiString class is built. In other words, the
AnsiString class is a wrapper around a simple C string, and the class
exists to make it easy to manipulate this string and to make the string
compatible with the needs of the VCL.
The following simple declaration would call the second constructor shown in
the previous list:
AnsiString S("Sam");
This is the typical method you would use when initializing a variable of type
AnsiString.
NOTE: I have included a second example
program called UsingAnsiString2, which features a small class that
overrides all the constructors for the AnsiString class:
class MyAnsiString : public AnsiString
{
public:
__fastcall MyAnsiString(void): AnsiString() {}
__fastcall MyAnsiString(const char* src): AnsiString(src) {}
__fastcall MyAnsiString(const AnsiString& src): AnsiString(src) {}
__fastcall MyAnsiString(const char* src, unsigned char len): AnsiString
(src, len) {}
__fastcall MyAnsiString(const wchar_t* src): AnsiString(src) {}
__fastcall MyAnsiString(char src): AnsiString(src) {}
__fastcall MyAnsiString(int src): AnsiString(src) {}
__fastcall MyAnsiString(double src): AnsiString(src) {}
};
This class is provided so you can step through the constructors to see
which ones are being called. For instance, the three constructors shown
immediately after this note have been rewritten in the UsingAnsiStrings2
program to use MyAnsiStrings rather than AnsiStrings. This
gives you an easy-to-use system for explicitly testing which constructors are
being called in which circumstance.
When I come up with a unit like this that may be of some general utility in
multiple programs, I usually put it in the utils subdirectory located
on the same level as the chapter subdirectories. In other words, I move or
copy it out of the directory where the files for the current program are
stored and place it in a subdirectory called utils that is on the
same level as the directories called Chap01, Chap02, and so
on.
You might need to add this directory to the include search path for your
project, or the program might not be able to find the MyAnsiString.h
unit. To set up the compiler for your system, go to Options | Project |
Directories/Conditionals and change the Include Path to point to the directory
where MyAnsiString.h is stored.
Sometimes I will leave one frozen copy of a unit in the directory where it was
first introduced and continue development of the copy of the unit that I place
in the utils subdirectory. That way, you can find one copy of the
file that looks the way you expect it to look in the same directory as the
program in which I introduce it, while continuing to develop the code in a
separate unit of the same name found in the utils directory. Check
the Readme.txt file on the CD that accompanies this book for further
information.
The following are a few simple examples from the UsingAnsiStrings program
that show examples of creating and using AnsiStrings:
void __fastcall TForm1::PassinCString1Click(TObject
*Sender)
{
AnsiString S("Sam");
Memo1->Text = S;
}
void __fastcall TForm1::PassInInteger1Click(TObject *Sender)
{
AnsiString MyNum(5);
Memo1->Text = MyNum;
}
void __fastcall TForm1::PassInaDouble1Click(TObject *Sender)
{
AnsiString MyDouble(6.6);
Memo1->Text = MyDouble;
}
This code demonstrates several things. The first, and most important point,
is that it shows how you can create an AnsiString object by
initializing it with a string, an integer, or a double. In short, these
constructors can automatically perform conversions for you. This means you can
usually write code like this:
AnsiString S;
int I = 4;
S = I;
ShowMessage(S);
When working with C strings, you always have to be careful that the variable
you have been working with has been properly initialized. This is not nearly as
big a concern when you are working with AnsiStrings. Consider the
following code:
void __fastcall TForm1::InitializetoZero1Click(TObject
*Sender)
{
AnsiString S;
Memo1->Text = S.Length();
}
The first line creates an AnsiString class that has a zero length
string. The second line performs a completely safe and legal call to one of the
methods of this AnsiString. This is the type of situation that can be
very dangerous in C. Here, for instance, is some code that is likely to blow up
on you:
char *S;
int i = strlen(S);
This code appears to do more or less the same thing as the code in the
InitializetoZero1Click method. In practice, however, this latter example
actually raises an access violation, while the AnsiString code example
succeeds. The explanation is simply that the declaration of the AnsiString
class created a real instance of an object called S. You can safely
call the methods of that object, even if the underlying string has no memory
allocated for it! The second example, however, leaves the char *
declared in the first line completely impotent, with no memory allocated for it.
If you want to avoid trouble, you should not do anything with that variable
until you allocate some memory for it.
In the last few paragraphs I have outlined an example illustrating what it is
I like about the AnsiString class. In short, this class makes strings
safe and easy to use.
NOTE: It goes without saying that C
strings are generally faster than AnsiStrings, and that they take up
less memory. Clearly, these are important features, and obviously I am not
stating that C strings are now obsolete.
The reasoning on this issue is a bit like that we undertake when deciding
whether to buy a car or a motorcycle. Cars are more expensive than
motorcycles, and they don't let you weave back and forth between lanes when
there is congested traffic. On the other hand, drivers of cars are much less
likely to end up in the hospital, and cars are much more pleasant to be in
during inclement weather. In the same way, AnsiStrings aren't as
small and flexible as C strings, but they are less likely to crash the system,
and they stand up better when you are in a rush or when handled by
inexperienced programmers.
This book focuses on ways to quickly write safe, high-performance programs. If
that is your goal, use AnsiStrings. If you are trying to write an
operating system, a compiler, or the core module for a 3D game engine, you
should probably concentrate more on speed than I do in this book and should
use AnsiStrings only sparingly.
Please note that my point here is not that you can't use C++Builder to write
highly optimized code, but only that this book usually does not focus on that
kind of project.
Sticky Constructor Issues
The AnsiString constructors are easy to use, but things can be a bit
confusing if you try to think about what is going on behind the scenes. For
instance, consider what happens when you pass an AnsiString to a
function:
AnsiString S;
MyFunc(S);
If the call to MyFunc is by value, not by reference, the constructor
for S is going to be called each time you pass the string. This is not
a tremendous burden on your program, but it is probably a bit more significant
weight than you had in mind to impose on your code. As a result, you should pass
in the address of the variable in most circumstances:
AnsiString S;
MyFunc(&S);
Even better, you should construct methods that declare all their string
variables as being passed by reference:
int MyFunc(AnsiString &S)
{
S = "The best minds of my generation...";
return S.Length();
}
This is like a var parameter in Object Pascal in that it lets you
pass the string by reference without worrying about pointers:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
AnsiString S;
int i = MyFunc(S);
ShowMessage(S + " Length: " + i);
}
Even when MyFunc changes the string, the result of the changes is
reflected in the calling module. This syntax passes a pointer to the function,
but makes it seem as though you are working with a local stack-based variable on
both the caller and calling sides.
Consider the following code samples:
MyAnsiString S = "The road to the contagious hospital";
MyAnsiString S1 = AnsiString("If I had a green automobile...");
MyAnsiString S2("All that came out of them came quiet, like the four
seasons");
ShowMessage(S + `\r' + S1 + `\r' + S2);
It should be clear from looking at this code that the second example will
take longer to execute than the third, because it calls two constructors rather
than just one. You might also think that the first takes longer than the third.
A logical course of reasoning would be to suppose that, at minimum, it would
have to call both a constructor and the equals operator. In fact, when I stepped
through the code, it became clear that the compiler simply called the
constructor immediately in the first example, and that the machine code executed
for the first and third examples was identical.
What is the lesson to be learned here? Unless you are writing a compiler, an
operating
system, or a 3D game engine, just don't worry about this stuff. Of course, if
you love worrying about these issues, then worry away. I suppose someone has to.
Otherwise, relax. There is almost no way to tell from looking at the code what
the compiler
is going to do, and 90 percent of the time the compiler is smart enough to
generate optimal code unless you explicitly tell it to go out of its way, as I
do in the second example.
If you are a beginning- or intermediate-level programmer, someone is going to
tell you that you should pull your hair out worrying about optimization issues
like this. Personally, I think there are more important subjects to which you
can dedicate your time. Eliminating one constructor call might save a few
nanoseconds in execution time, but nobody is going to notice it. Learn to
separate the petty issues from the serious issues.
Most importantly of all, wait until you have a program up and running before
you decide whether or not you have a performance bottleneck. If the program is
too slow, use a profiler to find out where the bottlenecks are. Ninety-eight
percent of the time the bottlenecks are in a few isolated places in the code,
and fixing those spots clears up the problem. Don't spend two weeks looking for
every place there is an extra constructor call only to find it improves your
performance by less than one percent. Wait till you have a problem, find the
problem, and then fix it. If it turns out that you are in a loop, and are making
extra constructor calls in the loop, and that by doing some hand waving to get
rid of the calls you improve program performance by 10 percent, then great. But
don't spend days working on "optimizations" that end up improving performance by
only one or two percent.
AnsiString Comparison Operators
Comparison operators are useful if you want to alphabetize strings. For
instance, asking if StringOne is smaller than StringTwo is a
means of finding out whether StringOne comes before StringTwo
in the alphabet. For instance, if StringOne equals "Dole" and
StringTwo equals "Clinton", StringOne is larger than
StringTwo because it comes later in the alphabet. (At last, a
politically controversial string comparison analysis!)
Here are the AnsiString class comparison operators and methods:
bool __fastcall operator ==(const AnsiString& rhs)
const;
bool __fastcall operator !=(const AnsiString& rhs) const;
bool __fastcall operator <(const AnsiString& rhs) const;
bool __fastcall operator >(const AnsiString& rhs) const;
bool __fastcall operator <=(const AnsiString& rhs) const;
bool __fastcall operator >=(const AnsiString& rhs) const;
int __fastcall AnsiCompare(const AnsiString& rhs) const;
int __fastcall AnsiCompareIC(const AnsiString& rhs) const; //ignorecase
The UsingAnsiString program on this book's CD-ROM shows how to use these
operators. For instance, the following method from that program shows how to use
the equals
operator:
void __fastcall TForm1::Equals1Click(TObject *Sender)
{
AnsiString StringOne, StringTwo;
InputDialog->GetStringsFromUser(&StringOne, &StringTwo);
if (StringOne == StringTwo)
Memo1->Text = "\"" + StringOne + "\" is equal to \"" + StringTwo + "\"";
else
Memo1->Text = "\"" + StringOne + "\" is not equal to \"" + StringTwo + "\"";
}
Here is an example of using the "smaller than" operator:
void __fastcall TForm1::SmallerThan1Click(TObject
*Sender)
{
AnsiString String1, String2;
InputDialog->GetStringsFromUser(&String1, &String2);
if (String1 < String2)
Memo1->Text = "\"" + String1 + "\" is smaller than \"" + String2 + "\"";
else
Memo1->Text = "\"" + String1 + "\" is not smaller than \"" + String2 + "\"";
}
The AnsiCompareIC function is useful if you want to ignore the case
of words when comparing them. This method returns zero if the strings are equal,
a positive number if the string calling the function is larger than the string
to which it is being compared, and negative number if it is not larger than the
string to which it is being compared:
void __fastcall TForm1::IgnoreCaseCompare1Click(TObject
*Sender)
{
AnsiString String1, String2;
InputDialog->GetStringsFromUser(&String1, &String2);
if (String1.AnsiCompareIC(String2) == 0)
Memo1->Text = "\"" + String1 + "\" equals \"" + String2 + "\"";
else if (String1.AnsiCompareIC(String2) > 0)
Memo1->Text = "\"" + String1 + "\" is larger than \"" + String2 + "\"";
else if (String1.AnsiCompareIC(String2) < 0)
Memo1->Text = "\"" + String1 + "\" is smaller than \"" + String2 + "\"";
}
Consider the following chart:
| StringOne |
StringTwo |
Result |
| England |
England |
Returns zero |
| England |
France |
Return a negative number |
| France |
England |
Returns a positive number |
-
Using the Pos Method
The Pos method is used to find the offset of a substring within a
larger string. Here is a simple example from the UsingAnsiString program of how
to use the Pos function:
void __fastcall TForm1::SimpleString1Click(TObject
*Sender)
{
AnsiString S = "Sammy";
Memo1->Text = S;
int i = S.Pos("mm");
S.Delete(i + 1, 2);
Memo1->Lines->Add(S);
}
This code creates an AnsiString initialized to the name "Sammy".
It then searches through the string for the place where the substring "mm"
occurs, and returns that index so that it can be stored in the variable i. I
then use the index as a guide when deleting the last two characters from the
string, thereby transforming the word "Sammy" to the string "Sam".
Here is a similar case, except that this time code searches for a tab rather
than an ordinary substring:
void __fastcall TForm1::Pos1Click(TObject *Sender)
{
AnsiString S("Sammy \t Mike");
Memo1->Text = S;
AnsiString Temp = Format("The tab is in the %dth position",
OPENARRAY(TVarRec, (S.Pos(`\t'))));
Memo1->Lines->Add(Temp);
}
The code in this example first initializes the AnsiString S
to a string that contains a tab, and then searches through the string and
reports the offset of the tab: "The tab is in the 7th position." The Format
function shown here works in the same fashion as sprintf. In fact, the
following code would have the same result as the Pos1Click method shown
previously:
AnsiString S("Sammy \x009 Mike");
Memo1->Text = S;
char Temp[75];
sprintf(Temp, "The tab is in the %dth position", S.Pos(`\t'));
Memo1->Lines->Add(Temp);
Escape Sequences in C++
C++ uses a series of escape sequences to represent special characters such as
tabs, backspaces, and so on. If you are not familiar with C++, you might find
the following table of escape sequences useful:
| Human readable name |
Escape sequence |
Hex representation |
| Bell |
\a |
\x007 |
| Backspace |
\b |
\x008 |
| Tab |
\t |
\x009 |
| Newline |
\n |
\x00A |
| Form Feed |
\f |
\x00C |
| Carriage return |
\r |
\x00D |
| Double quote |
\" |
\x022 |
| Single quote |
\' |
\x027 |
| Backslash |
\\ |
\x05C |
-
Escape sequences are usually placed in single quotes, though in the previous
case it would not matter whether I used single or double quotes in the Pos
statement.
Using hex values has the exact same effect as using escape sequences. For
instance, the following code creates identical output to the Pos1Click
method shown previously:
AnsiString S("Sammy \x009 Mike");
Memo1->Text = S;
AnsiString Temp = Format("The tab is in the %dth position",
OPENARRAY(TVarRec, (S.Pos(`\x009'))));
Memo1->Lines->Add(Temp);
Though some programmers may find it simpler and more intuitive to use the Hex
values shown in the last column of the table, the arcane-looking escape
sequences cling on in C++ code because they are portable to platforms other than
Windows and DOS.
Arrays of AnsiStrings
In this section you will see how to declare an array of AnsiStrings.
The text includes examples of how to access them as standard null-terminated
strings, and how to use them in moderately complex circumstances.
The following ChuangTzu program uses some Chinese quotes with a millennium or
two of QA under their belt to illustrate the favorite theme of this book:
Easy is right. Begin right
And you are easy.
Continue easy and you are right.
The right way to go easy
Is to forget the right way
And forget that the going is easy.
This program declares this string as an AnsiString, and then passes
it to a procedure that can break it down into a series of words. These words are
passed back one at a time and added into an array. The array is then displayed
in a series of one word lines in a memo control. The program appears in Listings
3.1 and 3.2.
Listing 3.1. The header for the ChuangTzu program.
//----------------------------------
#ifndef MainH
#define MainH
//----------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
//----------------------------------
class TChuangTzu
{
private:
int FCount;
AnsiString FWords[40];
AnsiString GetWords(int Index);
void SetWords(int Index, AnsiString S);
public:
TChuangTzu(){ FCount = 0; }
__property AnsiString Words[int Index] = {read=GetWords, write=SetWords};
__property int Count={read=FCount, write=FCount};
};
class TForm1 : public TForm
{
published:
TButton *Button1;
TMemo *Memo1;
void __fastcall Button1Click(
TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
//----------------------------------
extern TForm1 *Form1;
//----------------------------------
#endif
Listing 3.2. The main module of the ChuangTzu program.
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#include "codebox.h"
#pragma resource "*.dfm"
TForm1 *Form1;
///////////////////////////////////////
// ChuangTzu //////////////////////////
///////////////////////////////////////
AnsiString TChuangTzu::GetWords(int i)
{
return FWords[i];
}
void TChuangTzu::SetWords(int i,const AnsiString s)
{
FCount++;
FWords[i]=s;
}
AnsiString StripWords(AnsiString &S, char Token)
{
AnsiString TokenStr(Token);
AnsiString Temp1;
AnsiString Temp2 = S + ` `;
Temp1 = strtok(S.c_str(), TokenStr.c_str());
Temp2 = strrev(Temp2.c_str());
Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1));
S = strrev(Temp2.c_str());
return Temp1;
}
///////////////////////////////////////
// Form1 //////////////////////////////
///////////////////////////////////////
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::Button1Click(
TObject *Sender)
{
TChuangTzu C;
AnsiString S1;
int i = 0;
AnsiString S =
"Easy is right. Begin right ";
"And you are easy. "
"Continue easy and you are right. "
"The right way to go easy "
"Is to forget the right way "
"And forget that the going is easy.";
do
{
S1 = StripWords(S, ` `);
C.Words[i] = S1;
i++;
} while (S1 != "");
for (i = 0; i < C.Count; i++)
{
Memo1->Lines->Add(C.Words[i]);
}
}
The core of this program is the ChuangTzu class definition:
class TChuangTzu
{
private:
int FCount;
AnsiString FWords[40];
AnsiString GetWords(int Index);
void SetWords(int Index, AnsiString S);
public:
TChuangTzu(){ FCount = 0; }
__property AnsiString Words[int Index] = {read=GetWords, write=SetWords};
__property int Count={read=FCount, write=FCount};
};
At the heart of the object is the declaration for an array of 40 AnsiStrings:
AnsiString FWords[40];
You can access the second member of this array with code that looks like
this:
FWords[1] = "Let some sad trumpeter stand on the empty
streets at dawn."
AnsiString S = FWords[1];
The ChuangTzu object provides an array property that gives you
access to this private data. There is, of course, no reason why you can't
declare an array of AnsiStrings on their own without a supporting
object. However, I thought it might be helpful to see how you can use an array
in your program without breaking the rule about keeping the raw data of your
objects private.
Here is the declaration for the array property:
__property AnsiString Words[int Index] =
{read=GetWords, write=SetWords};
As you can see, it returns an AnsiString and can be indexed with an
integer value. (There is nothing to keep you from declaring array
properties that are indexed with AnsiStrings!)
Here is the get method for the array:
AnsiString TChuangTzu::GetWords(int i)
{
return FWords[i];
}
Notice that it takes a single integer as a parameter and returns an
AnsiString.
Here is the set method for the array:
void TChuangTzu::SetWords(int i,const AnsiString s)
{
FCount++;
FWords[i]=s;
}
This function takes both an integer and a string as parameters. In order to
call this method, you write
ChuangTzu.Words[i] = "peace proclaims olives of endless
age."
Here you can see the integer that is passed to SetWords, as well as
a quote from Shakespeare's 107 sonnet that is passed in the second parameter.
The FCount variable is used to track the number of entries in the
array. Note that properties provide an improvement over get and set methods by
letting you use simple array syntax.
Here is an example of how to declare a large AnsiString that extends
over several lines:
AnsiString S =
"Easy is right. Begin right ";
"And you are easy. "
"Continue easy and you are right. "
"The right way to go easy "
"Is to forget the right way "
"And forget that the going is easy.";
The StripWords function is used to peel off words from this quote
one at a time, starting at the front:
AnsiString StripWords(AnsiString &S, char Token)
{
AnsiString TokenStr(Token);
AnsiString Temp1;
AnsiString Temp2 = S + ` `;
Temp1 = strtok(S.c_str(), TokenStr.c_str());
Temp2 = strrev(Temp2.c_str());
Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1));
S = strrev(Temp2.c_str());
return Temp1;
}
There are two interesting things about this function. The first shows how to
concatenate strings with the plus operator, and the second shows how to use
standard C string functions on an AnsiString.
Notice the bit of syntax that adds a space onto the end of the string passed
to the function:
AnsiString Temp2 = S + ` `;
You can always use the plus operator to append or prefix information to an
AnsiString. This eliminates any need for you to use the strcat
function, though strcat is a likely candidate for the routine inside
the AnsiString implementation that actually performs the concatenation.
NOTE: The reason I append a space onto
the end of this string is to ensure that the AnsiString object that
underlies this C string makes a unique copy of it. If I simply assign the two
strings without changing them, the call to strrev or strtok
will affect both strings!
Note also that AnsiStrings support the += operator.
Therefore you don't have to write S = S + "Sam", but can write S
+= "Sam".
I do, however, use standard C functions to tokenize and reverse the strings:
Temp1 = strtok(S.c_str(), TokenStr.c_str());
Temp2 = strrev(Temp2.c_str());
Temp2.SetLength(Temp2.Length() - (Temp1.Length() + 1));
What the function does here is not important. Just notice that I can get at
the
underlying null-terminated string with the c_str() method
of the AnsiString class. Once I have access to the underlying string, I
can pass it to any of the standard C functions such as strrev,
strcat, strtok, and so on.
To get the length of a string, call the Length method of the
AnsiString class:
int I = MyString.Length();
I have gone on at some length about the AnsiString class because it
is used extensively throughout this book and throughout most BCB programs. The
key fact to remember is that AnsiStrings are compatible with the VCL.
For instance, you can assign an AnsiString directly to a VCL control:
AnsiString S("Doubting the filching age will steal his
treasure");
Edit1->Text = S;
You can also assign a regular C string to a VCL control, but that is because
the VCL string is represented on the C++ side as an AnsiString, which
can have its constructor called by a null-terminated string:
char S[] = {"Till either gorge be stuffed or prey be
gone"};
Memo1->Text = S;
NOTE: This time the quote is from
Shakespeare's Venus and Adonis, with this seemingly gruesome line appearing in
the following context:
Till either gorge be stuffed or prey be gone
Even so she kiss'd his brow, his cheek, his chin,
And where she ends she doth anew begin.
The bard's point being, I suppose, that her appetites were large.
In fact, you can take advantage of these constructors and assign an integer
directly to a VCL control:
Memo1->Text = 2;
This is considerably more freedom than the strongly typed Object Pascal
language would ever give you liberty to pursue.
Exploring SYSDEFS.H
This chapter is largely about the places where C++ and the VCL are woven
together. Most of the code that is used to accomplish this end is found in
SYSDEFS.H. SYSDEFS.H is the place where the developers put most of
the core system code needed to allow BCB to use the VCL native types. In
particular, if there was some element of Object Pascal not supported by C++ that
played a role in the VCL, solutions to that problem were mapped out in this
file. The only major exception to this rule is DSTRINGS.H, where the
AnsiString class is declared.
NOTE: SYSDEFS.H is to some
degree a testament to the power of the C++ language. It features classes and
templates that emulate features of Object Pascal that Delphi relies on the
compiler to implement. The advantage of having the compiler implement these
features is that it is possible to pick and choose exactly what kind of syntax
you want to use. Thus the elegance and simplicity of the Object Pascal
language. On the other hand, the extraordinary richness and flexibility of the
C++ object model is highlighted beautifully by the work done in SYSDEFS.H.
The following classes appear in SYSDEFS.H:
class __declspec(delphireturn) Currency : public
CurrencyBase
class __declspec(delphireturn) TDateTime : public TDateTimeBase
class __declspec(delphiclass) TObject
class TMetaClass
class __declspec(delphireturn) Set
class TVarArray
class TVarData
class TVarRec
template<class T> class OpenArray
class __declspec(delphireturn) Variant: public TVarData
class AutoCmd
class NamedParm
class Procedure: public AutoCmd
class Function: public AutoCmd
class PropertySet: public AutoCmd
class PropertyGet: public AutoCmd
typedef class TMemoryManager *PMemoryManager;
class THeapStatus;
Some of these classes, such as TObject and TMetaClass, have
already been discussed at length. Other classes such as Procedure,
Function, PropertyGet, and PropertySet are part of the
guts of the BCB-VCL interface, and are not worth discussing in this context. But
a few of these classes, notably Set, Currency, TDataTime,
Variant, and TMemoryManager play an important role in standard
BCB programming, and will therefore be covered in the next few pages. I will
also take a look at TVarRec and OpenArray, because they play
an important supporting role in the object model.
Formatting Text
In the next three sections, called TDateTime, TCurrency,
and OpenArrays, I explore some of the ways you can format text before
presenting it to the user. You should read through all three sections to get a
feeling for some of the options supplied by BCB.
Many tools for formatting text have been contributed by third-party VCL
vendors. There are many sites on the Web that provide links to third-party
toolmakers. To get started, visit the Link section of my Web site:
users.aol.com/charliecal, or go to www.borland.com.
TDateTime
The VCL comes equipped with a number of functions for working with the
current date or time, as shown by this excerpt from the online help:
| Date |
Returns the current date |
| DateTimeToStr |
Converts a value from time format to a string |
| DateTimeToString |
Converts a value from time format to a string |
| DateToStr |
Converts a value from date format to a string |
| DayOfWeek |
Returns the current day of the week |
| DecodeDate |
Decodes the specified date |
| DecodeTime |
Decodes the specifies time |
| EncodeDate |
Returns values specified in date format |
| EncodeTime |
Returns values specified in time format |
| FormatDateTime |
Formats a date and time using the specified format |
| Now |
Returns the current date and time |
| StrToDate |
Coverts a string to a date format |
| StrToDateTime |
Converts a string to a date/time format |
| StrToTime |
Converts a string to a time format |
| Time |
Returns the current time |
| TimeToStr |
Converts a time format to a string |
-
There is also a TDateTimeField that is used in database programming.
Many of these functions work with an object type called TDateTime
that is found in the SYSDEFS.H unit. Most of the rest of this section
is dedicated to exploring the uses of the TDateTime type. This type
exists in order to provide compatibility with the VCL routines listed at the
beginning of this section.
The following method shows two different ways of outputting the current date:
void __fastcall TForm1::CurrentDate1Click(TObject
*Sender)
{
TDateTime Date1, Date2 = Now();
AnsiString S;
DateTimeToString(S, "dddd, mmmm dd, yyyy", Date1.CurrentDate());
S += "\r" + Date2.FormatString("dddd, mmmm dd, yyyy");
ShowMessage(S);
}
The code shown in the CurrentDate1Click method outputs the following
text inside a RichEdit control:
Tuesday, January 07, 1997
Tuesday, January 07, 1997
NOTE: The TRichEdit control
from the Win95 page of the Component Palette provides supports for the RTF
format. There is no set limit on the amount of text you can display in a
TRichEdit control, and it supports the use of multiple fonts and colors
inside a single control. There are also routines for saving the loading and
saving the contents of the control, and for printing.
I initialize the first of the two TDateTime objects with the default
constructor, which automatically sets the date to Saturday, December 30, 1899.
Then set asks this object to retrieve the current date (not the default date),
which it passes to me in the form of a TDateTime object:
static TDateTime __fastcall CurrentDate();
A raw TDateTime object is obviously not something I can show to the
user, so I use the VCL DateTimeToString routine to convert it into a
string:
DateTimeToString(S, "dddd, mmmm dd, yyyy",
Date1.CurrentDate());
This routine takes a string in the first parameter, a format string in the
second parameter, and a TDateTime record in the third parameter.
NOTE: If you look in SYSDEFS.H,
you will see that TDateTime is declared delphireturn. That
is because this object needs to conform to the expectations of VCL routines
such as DateTimeToString. In other words, this is a classic example
of what use the BCB team made of delphireturn.
The second TDateTime object shown in this example is set equal to
the current date during its construction:
TDateTime Date2(Now());
Notice that I pass the VCL routine called Now to the TDateTime
constructor. Now returns a variable of type TDateTime that is
initialized to the current date and time. You can then use the FormatString
method of the TDateTime object to retrieve the current string:
S += "\n" + Date2.FormatString("dddd, mmmm dd, yyyy");
The FormatString method takes the same type of format string passed
to DateTimeToString.
Format strings are documented in the online help, but the basic idea behind
them is that if you pass in two letters, you get a number back; pass in three
letters, and you get an abbreviation back; and pass in four letters, and you get
a whole word back:
dd => 01
ddd => Sun
dddd => Sunday
The following method displays the current time in a specified format:
void __fastcall TForm1::CurrentTime1Click(TObject
*Sender)
{
TDateTime Date(Now());
RichEdit1->Text = Date.FormatString("h:nn:ss am/PM");
}
The result will look something like this:
12:27:56 PM
In this case, the PM is in large letters because I wrote it that way
in the format specifier. Had I written am/pm, the resulting string
would have looked like this: 12:27:56 pm. The rest of the format
specifiers works like this:
h => No leading zero
hh => Prefix a leading zero
In short, if you pass in one letter, you get back a string with no leading
zero; if you pass in two, a leading zero will be added automatically. Notice
that you pass in an n to specify the format for minutes. This is
because m has already been used for months.
Here is a table listing the various specifiers you can use:
-
-
| Format specifier |
Item |
| s |
Seconds |
| m |
Minutes |
| h |
Hour |
| d |
Day |
| m |
Month |
| y |
Year |
| am/pm |
Format for time |
| c |
Uses ShortDateFormat and LongTimeFormat |
| t |
Uses ShortTimeFormat |
| tt |
Uses LongTimeFormat |
| ddddd |
Display date using ShortDateFormat |
| dddddd |
Display date using LongDateFormat |
-
The various date and time formats shown in the last items will be explained
in just one moment.
The DateTimeString., DateString, and TimeString
methods of the TDateTime object do not require that you provide format
specifiers. Here, for instance, is an example of how to use the
DateTimeString method:
void __fastcall TForm1::DateandTime1Click(TObject
*Sender)
{
TDateTime Date(Now());
RichEdit1->Lines->Add(Date.DateTimeString());
}
On my system, the code shown here produces the following result:
1/7/97 12:31:24 PM
The format used in this case is global to the system. You can change these
settings by working with a series of global variables found in SYSUTILS.HPP:
extern System::AnsiString CurrencyString;
extern unsigned char CurrencyFormat;
extern unsigned char NegCurrFormat;
extern char ThousandSeparator;
extern char DecimalSeparator;
extern unsigned char CurrencyDecimals;
extern char DateSeparator;
extern System::AnsiString ShortDateFormat;
extern System::AnsiString LongDateFormat;
extern char TimeSeparator;
extern System::AnsiString TimeAMString;
extern System::AnsiString TimePMString;
extern System::AnsiString ShortTimeFormat;
extern System::AnsiString LongTimeFormat;
extern System::AnsiString ShortMonthNames[12];
extern System::AnsiString LongMonthNames[12];
extern System::AnsiString ShortDayNames[7];
extern System::AnsiString LongDayNames[7];
On my system, these values are preset as follows:
CurrencyString: $
CurrencyFormat: 0
NegCurrFormat: 0
ThousandSeparator: ,
DecimalSeparator: .
CurrencyDecimals: 2
DateSeparator: /
ShortDateFormat: M/d/yy
LongDateFormat: dddd, MMMM dd, yyyy
TimeSeparator: :
TimeAMString: AM
TimePMString: PM
ShortTimeFormat: h:mm AMPM
LongTimeFormat: h:mm:ss AMPM
ShortMonthNames: Jan
ShortMonthNames: Feb
ShortMonthNames: Mar
ShortMonthNames: Apr
ShortMonthNames: May
ShortMonthNames: Jun
ShortMonthNames: Jul
ShortMonthNames: Aug
ShortMonthNames: Sep
ShortMonthNames: Oct
ShortMonthNames: Nov
ShortMonthNames: Dec
LongMonthNames: January
LongMonthNames: February
LongMonthNames: March
LongMonthNames: April
LongMonthNames: May
LongMonthNames: June
LongMonthNames: July
LongMonthNames: August
LongMonthNames: September
LongMonthNames: October
LongMonthNames: November
LongMonthNames: December
ShortDayNames: Sun
ShortDayNames: Mon
ShortDayNames: Tue
ShortDayNames: Wed
ShortDayNames: Thu
ShortDayNames: Fri
ShortDayNames: Sat
LongDayNames: Sunday
LongDayNames: Monday
LongDayNames: Tuesday
LongDayNames: Wednesday
LongDayNames: Thursday
LongDayNames: Friday
LongDayNames: Saturday
For instance, the following code changes the nature of the current
ShortDateFormatString:
void __fastcall
TForm1::SetShortDateFormattoMMMMDDDDYYYY1Click(TObject *Sender)
{
RichEdit1->Text = Now();
ShortDateFormat = "MMMM/DDDD/YYYY";
RichEdit1->Lines->Add(Now());
}
The text displayed by calling this function is as follows:
1/7/97 1:30:21 PM
January/Tuesday/1997 1:30:21 PM
The first block of text shows the default behavior, and the second block
shows what happened after I made a few subtle changes to the system.
The system initializes the date and time strings to the choices you make in
Windows. You can see the current Windows settings by calling GetLocaleInfo.
Changes you make are written to the system registry if you send a
WM_INICHANGE message to your own nonconsole mode application.
The following lines of code show how you can compare two times using the
greater than operator:
void __fastcall TForm1::SetTimeOne1Click(TObject
*Sender)
{
FTimeOne = Now();
RichEdit1->Text = FTimeOne.TimeString();
}
void __fastcall TForm1::SetTimeTwo1Click(TObject *Sender)
{
FTimeTwo = Now();
RichEdit1->Lines->Add(FTimeTwo.TimeString());
}
void __fastcall TForm1::CompareOnetoTwo1Click(TObject *Sender)
{
AnsiString S;
if (FTimeOne > FTimeTwo)
S = FTimeOne.TimeString() + " > " + FTimeTwo.TimeString();
else
S = FTimeTwo.TimeString() + " > " + FTimeOne.TimeString();
RichEdit1->Lines->Add(S);
}
The first method shown here sets a global object called TimeOne to
the current time. The second method sets a second global object to the current
time. After calling the two functions at a two-second interval, you can end up
with the objects initialized to two different times:
2:10:29 PM
2:10:31 PM
You can then use the third function to compare them using the >
operator:
2:10:31 PM > 2:10:29 PM
You can also use the ++ operator on the current time to increment
the day by one:
void __fastcall TForm1::CompareTwotoOne1Click(TObject
*Sender)
{
int i;
RichEdit1->Lines->Add(FTimeOne.DateTimeString());
FTimeOne++;
RichEdit1->Lines->Add(FTimeOne.DateTimeString());
}
Here is the output in the RichEdit control after a run off all four
functions:
2:13:03 PM
2:13:05 PM
2:13:05 PM > 2:13:03 PM // First call to compare
1/7/97 2:13:03 PM
1/8/97 2:13:03 PM
2:13:03 PM > 2:13:05 PM // Second call to compare
As you can see, calling the ++ operator incremented the day by one.
After incrementing the values, a second call to the compare function showed that
FTimeOne is now larger than FTimeTwo. Even though it is still
two seconds smaller than date two, it is now a day later, and hence larger.
This should give you some sense of what you can do with the TDateTime
type. This has not been an exhaustive investigation of what can be done, but if
you now open up SysDefs.h, you should have no trouble following the
drift of the declaration for the other methods in the TDateTime object.
The code samples shown in this section are from a program found on disk called,
in a flight of poetic fancy, DateTime1.
TCurrency
I will now spend a few minutes looking at TCurrency. After the
rather lengthy investigation of TDateTime that you just saw, there
should not be much need to examine this object in-depth, because it follows the
same general patterns of logic you saw in the previous section.
When following this discussion, remember that the primary reason the type
exists is for compatibility with the VCL. In particular, the TCurrency
type, like the TDateTime type, is used in database programming. For
instance, there is a TCurrencyField and a TDateTimeField
available for database programmers. The principles behind the field types are
examined in detail throughout the lengthy database section of this book,
Chapters 8 through 18, and particularly in Chapter 11, "Working with Field
Objects."
The Currency type is a wrapper around the built in __int64
type. The compiler probably will not support assigning large numbers directly to
a variable of type Currency:
Currency C = 5000000000000; // This probably won't work
You can, however, pass in a string that holds a large number. Consider the
following method:
Currency C = void __fastcall
TForm1::Button1Click(TObject *Sender)
{
Currency C("500000000000000");
AnsiString S = C;
Edit1->Text = S;
Edit2->Text = Format("%m", OPENARRAY(TVarRec, (C)));
}
This method initializes a variable of type currency to a large number with a
string. Most of the time large numbers will be entered by the user in an edit
control, so this is a reasonable way to proceed.
Once you have an instance of a Currency type, you can display it
directly to the user or assign it to a TCurrencyField from a database
table. Notice that you can assign a string directly to a Currency
field, and vice versa. When translating a currency type to a string, the result
does not produce a formatted result string. Instead, you see a plain number:
500000000000000
If you want to display the string with proper formatting, use the Format
function, as shown in the Button1Click method:
$500,000,000,000,000.00
I will describe the Format function, and OpenArrays, in the
next section of this chapter, called "Working with OpenArrays." Note
that some of the database controls will automatically display a currency type
with formatting, depending in part on their attributes and the contents of the
Data Dictionary.
The currency type will handle numbers in the following range:
-922337203685477.5808..922337203685477.5807
That is, it can handle between 19 and 20 digits.
There are many operators in the Currency class for performing
mathematical operations. For instance, the following syntax is valid:
Currency C1 = 25000, C2 = 5000, C3;
C1 += C2;
C1 = C2 / 9;
C3 = (C1 % C2);
C3 = (C1 + C2) / C1 + (C1 % C2);
It's interesting to note that these operations are performed on objects, and
not on simple types. This is an excellent example of the power of C++ and the
amazing things you can do with operator overloading. If you are not used to C++,
you should spend a few moments contemplating the fact that C1, C2,
and C3 are objects, with methods and functions, and not just simple
built-in types like integers or floats. You should open up SysDefs.h to
see a list of all the supported syntax used by the Currency object.
MaskEdits and Other Issues
The native VCL TMaskEdit control deserves at least a few sentences
of comment during this discussion of formatting strings. This component is a
descendant of TEdit that enables you to control the kinds of characters
that are entered into the control.
The key to the TMaskEdit component is the EditMask
property, which enables you to enter a string that uses several special
characters to define what the user can legally enter into the edit area.
There is a property editor associated with the EditMask property. In
this editor, you can find a number of default masks, as shown in Figure 3.1.
However, on many occasions, none of these masks will suit your purpose.
Figure 3.1. The property editor for the EditMask property.
I could, for instance, enter the following string as an EditMask:
######;0;_
The purpose of this string is to ensure that the user can enter only numeric
values. In particular, these numeric values represent the number of widgets that
the user wants to sell.
To understand the EditMask property, you need to study the entry
associated with it in the online help. Here, you see all the characters that can
be used to define a mask, along with a description of the effects associated
with each character. I quote most of this list later in this section.
In the online help for the EditMask property, you will find that a
pound sign (#) enables you to enter only numbers, spaces, and plus and
minus characters. Notice, however, that there is also a 0 character,
which requires that you enter a number in each location marked with that
character. The question, then, is which special character should you use: #
or 0?
In some cases, the 0 character is not what you want, because it
forces the user to enter values. For instance, the following code forces the
user to enter a six-digit number, one for each of the zeroes you place in the
mask:
000000;0;_
The preceding mask would enable you to enter the number 123456, but
it would reject 123. If you want to give the user the freedom to enter
a number of any size up to 999,999, you should use the #
character rather than the 0 character. One way to sum this matter up is
as follows: The 0 character means that the user has to fill in that
place with a number, while the # character enables the user to fill in
that space with either a number or a blank space.
The zero that appears after the semicolon in the previous string specifies
that the mask entered is not saved as part of the data. If I had placed a 1
here, the mask would have been saved. In this particular case, there are no mask
characters to either save or discard, so it doesn't matter what I placed in this
location. The
phone mask, shown previously in Figure 3.1, contains two parentheses that
would be affected by this value.
Finally, the very end of the previous mask includes an underscore. This value
is used to specify what will be used to designate the space character.
Notice that each field of the mask is separated from the last by a semicolon.
Each mask consists of three fields, which were described earlier.
The MaskEdit component is available from the Additional page of the
Component palette. Notice that you can click a button in the EditMask
dialog found in the EditMask property editor in order to load
country-specific strings.
NOTE: The following lists format
specifiers you can use in the TMaskEdit control:
! If an ! character appears in the mask, leading blanks
don't appear in the data. If an ! character is not present, trailing
blanks don't appear in the data.
> If a > character appears in the mask, all characters that
follow are in uppercase until the end of the mask or until a <
character is encountered.
< If a < character appears in the mask, all characters that
follow are in lowercase until the end of the mask or until a >
character is encountered.
<> If these two characters appear together in a mask, no case
checking is done and the data is formatted with the case the user uses to
enter the data.
\ The character that follows a \ character is a literal
character. Use this character when you want to allow any of the mask special
characters as a literal in the data.
L The L character requires only an alphabetic character only
in this position. For the US, this is A-Z, a-z.
l The l character permits only an alphabetic character in
this position, but doesn't require it.
A The A character requires an alphanumeric character only in
this position. For the US, this is A-Z, a-z, and 0-9.
a The a character permits an alphanumeric character in this
position, but doesn't require it.
C The C character requires a character in this position.
c The c character permits a character in this position, but
doesn't require it.
0 The 0 character requires a numeric character only in this
position.
9 The 9 character permits a numeric character in this
position, but doesn't require it.
# The # character permits a numeric character or a plus or
minus sign in this position, but doesn't require it.
: The : character is used to separate hours, minutes, and
seconds in times. If the character that separates hours, minutes, and seconds
is different in the International settings of the Control Panel utility on
your computer system, that character is used instead of :.
/ The / character is used to separate months, days, and
years in dates. If the character that separates months, days, and years is
different in the International settings of the Control Panel utility on your
computer system, that character is used instead of /.
; The ; character is used to separate masks.
_ The _ character automatically inserts a blank the edit
box. When the user enters characters in the field, the cursor skips the blank
character. When using the EditMask property editor, you can change
the character used to represent blanks. You can also change this value
programmatically. See the following table.
Remember that many of the database objects such as the TDateTime
field provide automatic formatting for you.
This is also an area where you might consider turning to third-party
products. For instance, Turbo Power Software has an excellent library called
Orpheus that provides many fine controls and routines for automatically
formatting strings. You should, however, explore the market thoroughly, because
there are many fine products out there for handling this type of problem. In
particular, many controls are found on the Web. You can visit my Web site to
find links to sites that specialize in components. (users.aol.com/charliecal).
Working with OpenArrays
The VCL supports a concept called an array of const. This type will
enable you to pass in a variable number of parameters. Think for a moment of the
sprintf function:
int sprintf(char *buffer, const char *format[,
argument, ...]);
The last argument of this function can contain multiple parameters.
OpenArrays provide the same functionality in Object Pascal. In short, they
enable you to pass a variable number of parameters to a function.
Here is an Object Pascal code segment declared to accept an array of
const:
function Format(const Format: string; const Args: array
of const): string;
The Format function acts almost exactly like sprintf. Its
first parameter contains a string with various format specifiers in it, and its
second parameter consists of an array containing multiple values to be
formatted. The format specifiers used by Format are essentially the
same as those you would use in sprintf. After all, the whole point of
adding the function to the Object Pascal language was simply to duplicate the
functionality of sprintf inside Object Pascal. Here is the way a call
to Format looks in Object Pascal:
var
S, Name: string;
Age: Integer;
begin
Name = GetName;
Age = GetAge;
S := Format("My name is %s and I am %d years old", [Name, Age]);
end;
All this is good and well, but you might object that C++ already has support
for this kind of syntax through sprintf. That is correct, but you will
find many VCL database routines that use arrays of const for various
purposes. There are no standard C++ equivalents of these routines, as they exist
only in the VCL. In other words, the C++ and Object Pascal implementations of
this functionality differ in their details, even though the interface to the
user is similar. As a result, special C++ classes need to be created to
duplicate this built-in Object Pascal type.
While you may or may not find the Format routine itself useful, you
will definitely need to know about arrays of const if you want to work
with the VCL, and particularly if you want to do any database programming. For
instance, the following database routines all use arrays of const:
FinkKey, FindNearest, SetRange, AddRecord,
SetKeyFields, AppendRecord, InsertRecord, SetFields,
AssignValue, and DBErrorFmt. Clearly, this is a subject that
you need to understand.
Here is the declaration for the Format function as it appears in
SYSUTILS.HPP:
extern System::AnsiString __fastcall Format(const
System::AnsiString Format,
const System::TVarRec *Args, const int Args_Size);
As you can see, this function takes not two, but three parameters. You will
not, however, pass all three parameters into this function, nor will you pass
them into any of the database routines shown previously. Instead, you use a
macro called OPENARRAY, which enables you to call VCL functions that
use arrays of const with a reasonable degree of ease.
NOTE: As I am sure you have noticed
already, this particular subject is not exactly the garden spot of C++Builder.
We are definitely in the low-rent district right now, and so perhaps a few
more words of explanation are necessary.
Object Pascal originally had no support for functions that took a variable
array of parameters. As the spec for Delphi developed, it became clear that
there had to be a solution to this problem. The language needed to support
functions that took a varying array of parameters because various database
calls demanded this functionality. In particular, routines that dealt with
keyed fields needed varying numbers of parameters depending on the number of
fields present in a composite key.
An array of const was the solution the Object Pascal team came up
with for this problem. On the Delphi side, this is a elegant solution that
enables you to write simple, easy-to-read code. However, this is not the same
solution that the C and C++ languages used when resolving a similar problem.
As a result, arrays of const are implemented in an entirely different
manner than either the variadic arguments such as you find in
sprintf, or the standard C++ practice of function overloading. There was,
in short, no common ground between the two languages when it came to this
subject.
Hence you find your self wandering down this dark alley, where OpenArrays
meet the array of const. I won't make myself look foolish by trying
to claim that the solution found here is elegant, but it is usable. Given the
nature of the problem the BCB team had thrust upon them, the solution seems
reasonable to me.
I also do not feel that this particular low-rent district is typical of BCB
neighborhoods. I love BCB components, properties, and events. These are areas
where the tool really shines. Open arrays, on the other hand, are at best
nothing more than a satisfactory solution to a difficult problem.
One important point to note about the BCB environment, however, is that you
can use function overloading in some cases, OPENARRAYS in other
cases, or switch to Object Pascal code and use elegant and concise solution
provided by an array of const. This environment provides a rich set
of tools from which you can pick and choose. Because you have access to the
tools available in two different languages, there is no other programming
environment on the market that even comes close to the richness of features
found in BCB.
Here is a routine that uses both open arrays and sprintf to solve
the same problem:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
char S[150];
if (MoreInput->ShowModal() == mrOk)
{
sprintf(S, "Hello, my name is %s, and I'm %d years old.",
MoreInput->Name, MoreInput->Age);
Edit1->Text = S;
Edit2->Text = Format("Hello, my name is %s, and I'm %d years old.",
OPENARRAY(TVarRec, (MoreInput->Name, MoreInput->Age)));
}
}
In showing your comparative examples of sprintf and Format,
this function makes use of a second form called TMoreInput. I will
discuss this form in the next section of the chapter. For now, however, I will
forgo discussing multiform projects, and will concentrate instead on using
OpenArrays.
As you can see, it doesn't really take any more code to use Format
and OPENARRAY than it does to use sprintf. In fact, both
techniques have their disadvantages. The sprintf method requires you to
declare a temporary variable, the Format method gives you a good dose
of the wordy OPENARRAY macro. However, neither technique is
particularly difficult to implement.
When using OPENARRAY, you usually pass TVarRec as the first
argument of the macro, and then pass a comma-delimited set of variables in the
second argument. The entire second argument should be set off by parentheses.
Here are a few rather artificial examples of how to use the macro:
OPENARRAY(TVarRec, (1))
OPENARRAY(TVarRec, (1, 2))
OPENARRAY(TVarRec, (1, 2, 3))
And so on, up to, but not beyond, 18. The point is that you pass the
variables you want to have processed in the second argument to the macro. You
can pass any type in this section, because the macro itself doesn't care. The
method that is using the macro may not be so forgiving, however, so you need to
put some thought into the code you write. For instance, Format takes
only certain types of parameters, and there would be no point in passing
something to OPENARRAY that the Format function could not
process. The same holds true of sprintf. There would be no point in
passing the wrong type of parameter to it, because the
format specifiers handle only certain types.
The OPENARRAY macro itself is very simple:
#define OPENARRAY(type, values) \
OpenArray<type>values, OpenArrayCount<type>values.GetHigh()
As you can see, it takes two separate object templates to make the array
work. (In fact, TVarRec is a third object, so it takes three objects in
all to bring this business home for dinner.) If you are interested, you can open
up SysDefs.h and see how this technology is implemented. For the
purposes of this book, however, I will simply say that this is one of those
cases where you should "pay no attention to the man behind the curtain." I just
let the macro do its work and concentrate instead on the parts of the program
that are under my jurisdiction.
That is all I'm going to say about OpenArrays. As I conceded
earlier, this is not really the garden spot of BCB programming. However, the
code I've shown you does get the job done. In particular, the point of this
OpenArray business is that it enables you to pass a variant number of
parameters to a function. This is a pretty slick trick in itself, and some
programmers will probably be able to find occasions when they can use this
technology for their own purposes. Its main value, however, is simply that it
enables you to use the parts of the VCL that are expecting an array of const.
Using Two Forms in One Program
The sample function shown above called Button1Click has a line in it
where a second form is launched:
if (MoreInput->ShowModal() == mrOk)
The program uses this second form to retrieve the name and age input from the
user.
Popping up a form to get information from a user is a common task in BCB, so
I should perhaps take a moment to explain how it works.
To get started, you should use the File menu, or some other tool, to create a
second form for your project. Save it into the same directory where your current
project is located. Add two edit controls labeled Name and Age
to the second form, and place two TBitBtns on the form, as shown in
Figure 3.2. Use the Kind property of the BitBtns to make one
an OK button and the second a Cancel button. Notice that the ModalResult
property of the OK TBitBtn is automatically set to mrOk, and
the ModalResult property of the Cancel TBitBtn is
automatically set to mrCancel.
Figure 3.2.The form for the TMoreInput dialog has two labels,
two edit controls, and two TBitBtns.
Now create two properties that users of the form can use when they need to
access the input from the user:
class TMoreInput : public TForm
{
// Code omitted here
private:
AnsiString FName;
int FAge;
public:
virtual __fastcall TMoreInput(TComponent* Owner);
__property int Age={read=FAge, write=FAge};
__property AnsiString Name={read=FName, write=FName};
};
As you can see, the Age and Name properties front for two
private variables of the object. These variable are assigned to the
user's input if the user presses the OK button:
void __fastcall TMoreInput::OkBtnClick(TObject *Sender)
{
Name = Edit1->Text;
Age = Edit2->Text.ToInt();
}
It is now time to go full circle and look back at the stripped-down version
of the function that makes use of this dialog:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if (MoreInput->ShowModal() == mrOk)
{
Edit2->Text = Format("Hello, my name is %s, and I'm %d years old.",
OPENARRAY(TVarRec, (MoreInput->Name, MoreInput->Age)));
}
}
The Button1Click method calls the TForm ShowModal
method to launch the dialog. If the user presses the OK button, the input is
retrieved from the TMoreInput dialog through the officially designated
properties. This enables you to use the private data of TMoreInput
without forcing TMoreInput to refrain from ever changing its
implementation. For instance, TMoreInput could someday use a struct
that contains both the Name and Age fields, but could continue
to give users of the form access to data through the Name and Age
variables.
If you are interested in this program, you can find it on disk in the
Chap02 directory as a file called OpenArrays1. The program itself
is simple, but it gives a good illustration of how to use a second dialog to
retrieve information from the user. This is a common task that will see many
variations run on it in different parts of this book.
Working with the Set Object
Sets are used widely in BCB. Here for instance, is the MsgDialog
routine, which is called frequently but requires some knowledge of sets before
you can use it correctly:
AnsiString S("While Lust is in his pride, no
exclamation \n"
"Can curb his heat or rein his rash desire.");
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK, 0);
The VCL makes heavy use of sets, so there are a number of places in standard
BCB database or interface code where you will need to take advantage of the
syntax you see here. The goal of the next few pages of this chapter is to
explore the BCB Set
class and reveal its inner workings.
Sets are easy to understand if you just take a few minutes to absorb their
syntax. This is, however, one of those subjects that you have to master if you
want to use BCB. In fact, you will find many parts of BCB cumbersome if you
don't understand this template class.
Here is a "pseudo template" for the header of the set class designed to help
you see how these sets are structured:
Set<Type, MinimumValue, MaximumValue>
More precisely, here is the more formal header declaration from SysDefs.h:
template<class T, unsigned char minEl, unsigned char
maxEl>
To declare a set, you write the word Set followed by an open
bracket, the type of your set, and its minimum and maximum values. For instance,
if you want to create a set of all small letters, you would write
Set<char, `a', `z'> SmallLetters;
To create the set of all numbers between 0 and 9, you might write
Set<int, 0, 9> SmallNumbers;
In the first case, MySet does not consist of the letters a
and z, but of a, z, and all the letters between them.
In other words, the letter a is the smallest value in the set and the
letter z is the maximum value in the set, as shown previously in the
formal declaration for the template class.
Both SmallLetters and SmallNumbers are declarations of
objects. Often, you don't want to create a new object, but create a new object
type with the typedef keyword:
typedef Set<int, 0, 9> TSmallNumbers;
Now you have a type you can work with, and can therefore declare separate
instances of this type:
TSmallNumbers SmallNumbers1;
TSmallNumbers SmallNumbers2;
The point here is that the first numeric set declared previously, the one
called SmallNumbers, is an instance of an object, while
TSmallNumbers is the declaration for a type.
NOTE: I'm going to break a rule of this
book and spend a few moments on standard C syntax. In particular, I'm going to
look at typedefs, because this is an important and easily
misunderstood part of the language. The typedef keyword enables you
to declare a type within a particular scope. Unfortunately, the #define
directive performs a function similar to the typedef keyword, even
though the two pieces of syntax have different ways of implementing the
result.
For instance, the following two lines have the same effect on your code:
typedef float REAL;
#define float REAL
One asks the compiler to do its work for it, while the second asks the
preprocessor to do the work. The result, however, is similar. That is, they
give you a new "type" to work with called REAL.
I try to keep some order in my code by always using typedef to
declare types, while I generally use #define only to declare
constants. No one is perfect, and I probably break that rule from time to
time, but I try to follow it with some regularity.
A typedef has effect only within a particular scope. I generally
declare types in the header for a unit, unless I want to limit the scope of
the type to a particular method or function, in which case I declare the type
inside that method or function.
I will not go into this subject any further in this book, nor is the
discussion included here meant to be exhaustive. If you want additional
information on the typedef keyword, you should turn to a primer on
C++. For this book all you need to know is that typedefs provide a
way of declaring your own types within a particular scope. I never use it for
any other purpose, and I try never to use any other technique for declaring
types.
Now that you know the basic facts about sets, it's time to look at some
programs that give them a workout. These are important examples, so I will
feature them in their own section of the chapter.
Two Sample Programs for Working with Sets
The sample program shown in Listings 3.3 and 3.4, called NumberSets, gives
you a look at the basic syntax for creating sets. It is followed by a second
sample program, called SetBasics, that shows how to create the union,
intersection, and difference of two sets.
NOTE: The Set sample programs used in
this book originally included some sample components that are found in the
Examples directory that ships with BCB. These components were found in
Delphi on a page called Samples, but are not installed by default in BCB. If
you go into the Examples\Controls directory, you will find an
explanation of how to install these components.
In particular, the spin edit control can be a useful tool to use in this kind
of program. Check the CD that accompanies this book for an update on this
issue and how it affects these programs.
Listing 3.3. The header for the NumberSets program.
//----------------------------------
#ifndef MainH
#define MainH
//----------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Spin.hpp>
//----------------------------------
typedef Set<int, 0, 9> TSmallNum;
class TForm1 : public TForm
{
__published:
TButton *AddToSetABtn;
TListBox *ListBox1;
TSpinEdit *SpinEdit1;
TButton *RemoveFromSetBtn;
void __fastcall AddToSetABtnClick(TObject *Sender);
void __fastcall RemoveFromSetBtnClick(TObject *Sender);
private:
TSmallNum NumSet;
void __fastcall ShowSet(TSmallNum ANumSet, int ListBoxNum);
public:
virtual __fastcall TForm1(TComponent* Owner);
};
//----------------------------------
extern TForm1 *Form1;
//----------------------------------
#endif
Listing 3.4. The main form for the NumberSets program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::ShowSet(TSmallNum ASmallNum, int ListBoxNum)
{
AnsiString S("Set <");
for (int i = 0; i < 275; i++)
{
if (ASmallNum.Contains(i))
S += "`" + AnsiString(i) + "`,";
}
S.SetLength(S.Length() - 1);
S += ">";
if (S.Length() == 5)
S = "NULL Set";
switch (ListBoxNum)
{
case 1:
ListBox1->Items->Add(S);
break;
default:
ShowMessage("Error specifying listbox");
}
}
void __fastcall TForm1::AddToSetABtnClick(TObject *Sender)
{
int i = SpinEdit1->Text.ToInt();
NumSet << i;
ShowSet(NumSet, 1);
}
void __fastcall TForm1::RemoveFromSetBtnClick(TObject *Sender)
{
int i = SpinEdit1->Text.ToInt();
NumSet >> i;
ShowSet(NumSet, 1);
}
NumberSet works with a simple set containing the numbers between 0 and 9:
typedef Set<int, 0, 9> TSmallNum;
TSmallNum NumSet;
The main function of the program is to show how to move items in and out of a
set. The AddToSetABtnClick method retrieves the number the user places
in a TSpinEdit control:
int i = SpinEdit1->Text.ToInt();
It then uses the << operator to insert the new number into the set:
NumSet << i;
A similar method shows how to move information out of the set:
int i = SpinEdit1->Text.ToInt();
NumSet >> i;
The most complicated code in the program involves a method called ShowSet
that creates a picture of the set and shows it to the user. The heart of the
ShowSet method uses the Contains method of BCB's Set
class to see if a particular element is contained in the set:
if (ASmallNum.Contains(i))
Contains returns True if the element is part of the set,
and it returns False if the element is not in the set. The results of
this effort are shown to the user in a TListBox, as illustrated in
Figure 3.3.
Figure 3.3.The NumberSet program creates a visual image of a set so you
can get an accurate feeling for how sets work.
The SetBasics program takes the principles used in the NumberSet example and
pushes them a little further so you can get a clear sense of what it means to
create the intersection, union, or difference of two sets. (See Figure 3.4.) The
code for the program is shown in Listing 3.5 and 3.6.
Figure 3.4.The SetBasics program shows how to find the union, difference,
or intersection of two sets.
Listing 3.5. The header for the SetBasics program.
//----------------------------------
#ifndef MainH
#define MainH
//----------------------------------
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
//----------------------------------
typedef Set <char, `A', `z'> TLetterSet;
class TForm1 : public TForm
{
__published:
TEdit *Edit1;
TButton *AddToSetABtn;
TListBox *ListBox1;
TButton *AddToSetBBtn;
TListBox *ListBox2;
TListBox *ListBox3;
TMainMenu *MainMenu1;
TMenuItem *Options1;
TMenuItem *Union1;
TMenuItem *Intersection1;
TMenuItem *Difference1;
void __fastcall AddToSetABtnClick(TObject *Sender);
void __fastcall AddToSetBBtnClick(TObject *Sender);
void __fastcall Union1Click(TObject *Sender);
void __fastcall Intersection1Click(TObject *Sender);
void __fastcall Difference1Click(TObject *Sender);
private:
TLetterSet LetterSetA;
TLetterSet LetterSetB;
void __fastcall ShowSet(TLetterSet ALetterSet, int ListBoxNum);
public:
virtual __fastcall TForm1(TComponent* Owner);
};
//----------------------------------
extern TForm1 *Form1;
//----------------------------------
#endif
Listing 3.6. The main form for the SetBasics program.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::ShowSet(TLetterSet ALetterSet, int ListBoxNum)
{
AnsiString S("Set <");
for (int i = 0; i < 275; i++)
{
if (ALetterSet.Contains(char(i)))
S += "`" + AnsiString(char(i)) + "`,";
}
S.SetLength(S.Length() - 1);
S += ">";
if (S.Length() == 5)
S = "NULL Set";
switch (ListBoxNum)
{
case 1: ListBox1->Items->Add(S); break;
case 2: ListBox2->Items->Add(S); break;
case 3: ListBox3->Items->Add(S); break;
default:
ShowMessage("Error specifying listbox");
}
}
void __fastcall TForm1::AddToSetABtnClick(TObject *Sender)
{
AnsiString S(Edit1->Text);
char ch = S[1];
LetterSetA << ch;
ShowSet(LetterSetA, 1);
}
void __fastcall TForm1::AddToSetBBtnClick(TObject *Sender)
{
AnsiString S(Edit1->Text);
char ch = S[1];
LetterSetB << ch;
ShowSet(LetterSetB, 2);
}
void __fastcall TForm1::Union1Click(TObject *Sender)
{
TLetterSet Union;
Union = LetterSetA + LetterSetB;
ShowSet(Union, 3);
}
void __fastcall TForm1::Intersection1Click(TObject *Sender)
{
TLetterSet Intersection;
Intersection = LetterSetA * LetterSetB;
ShowSet(Intersection, 3);
}
void __fastcall TForm1::Difference1Click(TObject *Sender)
{
TLetterSet Difference;
Difference = LetterSetA - LetterSetB;
ShowSet(Difference, 3);
}
//----------------------------------
The SetBasics program uses a set of char instead of a set of integers:
typedef Set <char, `A', `z'> TLetterSet;
The code for moving elements in a set has not changed significantly from the
NumberSet example:
void __fastcall TForm1::AddToSetABtnClick(TObject
*Sender)
{
AnsiString S(Edit1->Text);
char ch = S[1];
LetterSetA << ch;
ShowSet(LetterSetA, 1);
}
void __fastcall TForm1::AddToSetBBtnClick(TObject *Sender)
{
AnsiString S(Edit1->Text);
char ch = S[1];
LetterSetB << ch;
ShowSet(LetterSetB, 2);
}
The difference here, of course, is that two sets are being used, and that you
can only add elements to each set.
NOTE: Just before the release of
C++Builder, the team changed the way the first character of an AnsiString
is accessed. It is now indexed at the first position in the array, rather than
at position zero:
char ch = S[1]; // First character of string
char ch = S[0]; // References nothing, incorrect reference!
This change came so late in the product cycle that I may not reference it
correctly in all cases where the subject is brought up in this text. However,
I should have caught all references in the code that appears on the CD-ROM
that accompanies this book.
Once you have created two sets that interest you, you can press a button on
the applications form to see the union, difference, or intersection of the sets.
Here, for instance, is how to create the intersection of two sets:
void __fastcall TForm1::Intersection1Click(TObject
*Sender)
{
TLetterSet Intersection;
Intersection = LetterSetA * LetterSetB;
ShowSet(Intersection, 3);
}
The * operator of the Set template is used to perform the
operation. You use the + operator to find the union of two sets and the
- operator to show the difference between two sets:
Union = LetterSetA + LetterSetB;
Difference = LetterSetA - LetterSetB;
The main form for the program is shown in Listing 3.6.
Suppose you have two sets that look like this:
<`A', `B', `E'> <`B', `G'>
The union of the sets would look like this: <`A',
`B', `E', `G'>.
The intersection would look like this: <`B'>.
The difference, if you subtracted the second from the first, would look like
this: <`A', `E'>.
You can experiment with the program further if you want to see more about
creating the intersection, union, or difference of two sets. If you look in
SysDefs.h or in the online help, you will see that there are also operators
that enable you to test for equality or to see if one set is larger than
another.
A Few Additional Points about Sets
You can add multiple items to a set with the following syntax:
NumSet << 2 << 3 << 5;
Here, for instance, is a real-world case of adding or removing items from a
TNavigator
control:
FMyNavigator->VisibleButtons =
TButtonSet() << nbFirst << nbLast << nbNext << nbPrior;
In general, sets are very easy to use, though their syntax is not
particularly inviting at first glance. Once again, it is important to remember
that this Set class is meant for use with the VCL. The Set
class in SysDefs.h is not a general purpose set class, and it is not
meant as a substitute for the code you find in the Standard Template Library.
As with much of the code shown in this chapter, the key point that justifies
the existence of the Set class is its compatibility with certain
features of the VCL. Two such features have been illustrated in the last few
pages. The first illustrative example is the preceding FMyNavigator
code, and the MsgDialog code shown back at the beginning of this
discussion of sets:
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbOK,
0);
TMsgDlgButtons is a set class. It has the following declaration:
enum TMsgDlgBtn { mbYes, mbNo, mbOK, mbCancel, mbAbort,
mbRetry, mbIgnore, mbAll, mbHelp };
typedef Set<TMsgDlgBtn, mbYes, mbHelp> TMsgDlgButtons;
It should now be obvious how to use this set. The elements of the set consist
of all the items in the TMsgDlgBtn enumerated type. The set itself
ranges from a low value of mbYes to a high value of mbHelp. If
you wanted All and Ignore buttons on a MessageBox, you could write the
following code to display it to the user:
MessageDlg(S, mtInformation, TMsgDlgButtons() << mbAll
<< mbIgnore, 0);
The second parameter to MessageDlg contains a value from the
following enumerated type:
enum TMsgDlgType { mtWarning, mtError, mtInformation,
mtConfirmation, mtCustom };
Use the Source, Luke!
You might notice that I frequently refer directly to the various header files
that ship with BCB. Getting to know as much as possible about the source for the
VCL and for BCB is a worthwhile endeavor. There are times when I use the Tool
menu to make it even easier to get at source files that I use a lot.
You can configure the Tools menu in BCB to run any program you like. As a
result, I sometimes ask it to start Notepad (also known as Visual Notepad) or
some other editor with the source from a key header file in it. To do this
- 1. Open the Tools menu and select Configure.
2. In the Title field, give your new menu item a name, such as SysDefs.
3. In the Program field, enter the name of the executable you want to run,
such as Notepad, Visual SlickEdit, CodeWright, or MultiEdit.
4. Set up a working directory, which is usually the place where the
include files are located: g:\bcb\include\vcl.
5. Pass in the name of the file you want to launch in the Parameters
field.
6. Click the Close button.
The way this process looks in action is shown in Figure 3.5.
Figure 3.5.Using the Tools menu to create a means for automatically
opening a header file whenever you need it.
If possible, and especially if you are using Windows 95, you should find a
real editor to use with large header files. Good ones include Visual SlickEdit
from MicroEdge, MultiEdit from American Cybernetics, and CodeWright from Premia.
Getting the Heap Status
To understand what is happening in your program, you should become familiar
with the THeapStatus class from SYSDEFS.HPP:
class THeapStatus {
public:
Cardinal TotalAddrSpace;
Cardinal TotalUncommitted;
Cardinal TotalCommitted;
Cardinal TotalAllocated;
Cardinal TotalFree;
Cardinal FreeSmall;
Cardinal FreeBig;
Cardinal Unused;
Cardinal Overhead;
Cardinal HeapErrorCode;
} ;
To retrieve an instance of this type, you can call GetHeapStatus:
THeapStatus HeapStatus = GetHeapStatus();
The values retrieved by a call to GetHeapStatus tell you about the
condition of the heap for your program. They do not refer to global memory for
the machine on which your program is running. This call will probably fail if
made from inside a DLL.
Variants
Variants are an OLE-based type in the VCL that can be assigned to a wide
range of variable types. Some people jokingly refer to variants as a typeless
type, because it can be used to represent a string, an integer, an object, or
several other types.
The following is a simple procedure that illustrates the flexible nature of
variants:
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
Variant V;
V = 1;
V = "Sam";
}
This procedure compiles fine under BCB. As you can see, you are allowed to
assign both a string and an integer to the same variant variable.
The following procedure is also legal:
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
Variant V;
V = 1;
Caption = "Sam" + V;
}
The attempt to assign Edit1.Text to a variant concatenated with a
string would have been flagged as a type mismatch by Delphi because a variant in
Object Pascal takes on some of the attributes of an integer after being assigned
to that type. The Variant class implemented in SYSDEFS.H,
however, allows this kind of behavior and handles it properly.
Underneath the surface, variant types are represented by a 16-byte structure
found in SYSDEFS.H. At the time of this writing, that structure looks
like this:
class TVarData {
public:
Word VType;
Word Reserved1;
Word Reserved2;
Word Reserved3;
union {
Smallint VSmallint;
Integer VInteger;
Single VSingle;
Double VDouble;
CurrencyBase VCurrency;
TDateTimeBase VDate;
PWideChar VOleStr;
Ole2::IDispatch* VDispatch;
Integer VError;
WordBool VBoolean;
Ole2::IUnknown* VUnknown;
Byte VByte;
Pointer VString;
PVarArray VArray;
Pointer VPointer;
};
};
Notice that this is a union; that is, the various types represented in the
switch statement are overlaid in memory. The structure ends up being 16
bytes in size because the VType field is 2 bytes; the three reserved
fields total 6 bytes; and the largest of the types in the variant section is the
double, which is 8 bytes in size. (2 + 6 + 8 = 16.) Once again, the switch
statement in the declaration is not a list of separate fields, but a list of
different ways to interpret the 8 bytes of data contained in the second half of
the space allocated for the record.
The following are the declarations for the values used with variants:
#define varEmpty (unsigned char)(0)
#define varNull (unsigned char)(1)
#define varSmallint (unsigned char)(2)
#define varInteger (unsigned char)(3)
#define varSingle (unsigned char)(4)
#define varDouble (unsigned char)(5)
#define varCurrency (unsigned char)(6)
#define varDate (unsigned char)(7)
#define varOleStr (unsigned char)(8)
#define varDispatch (unsigned char)(9)
#define varError (unsigned char)(10)
#define varBoolean (unsigned char)(11)
#define varVariant (unsigned char)(12)
#define varUnknown (unsigned char)(13)
#define varByte (unsigned char)(17)
#define varString (unsigned short)(256)
#define varTypeMask (unsigned short)(4095)
#define varArray (unsigned short)(8192)
#define varByRef (unsigned short)(16384)
NOTE: When you study these
declarations, it's important to understand that the VCL defines a certain set
of behavior to be associated with variants, but might not guarantee that the
implementation will remain the same from version to version. In other words,
you can almost surely count on the fact that assigning both a string and an
integer to the same variant will always be safe. However, you might not
necessarily be sure that variants will always be represented by the same
constants and record shown previously. To check whether these implementations
have been blessed as being permanent, refer to your VCL documentation.
If I were writing the documentation for the VCL, I might decide not to show
you the specific record shown previously because these kinds of details might
change in later versions of the product, just as the internal representation
for long strings might change. However, in this book I do not claim to be
defining the language in a set of official documents. Instead, I am explaining
the product to you, using the techniques that seem most useful. In this
particular case, I think it helps to see behind the scenes and into the
particulars of this implementation of the Variant type. Whether or not this
will become a documented fact or an undocumented trick is not clear at the
time of this writing.
It should be clear from the preceding code examples that knowing what type is
represented by a variant at a particular moment can be important. To check the
variant's type, you can use the VarType function, as shown here:
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
int AType;
Variant V;
AType = VarType(V);
// Additional code
}
In this example, the Integer variable AType will be set to
one of the constants shown previously. In particular, it will probably be
assigned to varEmpty, because in the preceding example this variable
has not yet been assigned to another type.
If you want to get a closer look at a variant, you can typecast it to learn
more about it, as shown in the following example:
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
Variant V;
TVarData VarData;
int AType;
V = 1;
VarData = TVarData(V);
AType = VarData.VType;
}
In this particular case, AType will be set to the same value that
would be returned from a call to VarType.
To understand more about how all this works, see Listing 3.7 and 3.8, in
which you can find the code for the VarDatas program. This code sets a single
variant to a series of different values and then examines the variant to
discover the current value of its VType field.
Listing 3.7. The VarDatas program lets you examine
variant types. Here is the header file for the main form of the program.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
class TForm1 : public TForm
{
__published:
TButton *bVariantPlay;
TListBox *ListBox1;
void __fastcall bVariantPlayClick(TObject *Sender);
private:
void ShowVariant(Variant &V);
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 3.8. The main source file for the VarDatas
program includes a routine that returns a string stating the type of a variant.
///////////////////////////////////////
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
AnsiString GetVariantType(Variant &V)
{
TVarData VarData;
AnsiString S;
VarData = TVarData(V);
switch (VarData.VType)
{
case varEmpty: S = "varEmpty"; break;
case varNull: S = "varNull"; break;
case varSmallint: S = "varSmallInt"; break;
case varInteger: S = "varInteger"; break;
case varSingle: S = "varSingle"; break;
case varDouble: S= "varDouble"; break;
case varCurrency: S = "varCurrency"; break;
case varDate: S = "varDate"; break;
case varOleStr: S = "varOleStr"; break;
case varDispatch: S = "varDispatch"; break;
case varError: S = "varError"; break;
case varBoolean: S = "varBoolean"; break;
case varVariant: S = "varVariant"; break;
case varUnknown: S = "varUnknown"; break;
case varString: S = "varString"; break;
case varTypeMask: S = "varTypeMask"; break;
case varByRef: S = "varByRef"; break;
case varByte: S = "varByte"; break;
case varArray: S = "varArray"; break;
default:
S = "Error";
}
return S;
}
void TForm1::ShowVariant(Variant &V)
{
AnsiString S, Temp;
if ((VarIsEmpty(V) == True) || (VarIsNull(V) == True))
{
Temp = "Null";
S = Format("Value: %-15s Type: %s",
OPENARRAY(TVarRec, (Temp, GetVariantType(V))));
}
else
{
Temp = V;
S = Format("Value: %-15s Type: %s",
OPENARRAY(TVarRec , (Temp, GetVariantType(V))));
}
ListBox1->Items->Add(S);
}
void __fastcall TForm1::bVariantPlayClick(TObject *Sender)
{
Variant V;
ShowVariant(V);
V = Null;
ShowVariant(V);
V = 1;
ShowVariant(V);
V = "Sam";
ShowVariant(V);
V = 1.25;
ShowVariant(V);
}
A typical run of this program is shown in Figure 3.6.
Figure 3.6.The VarDatas program uses the TVarData structure to
examine how variants are put together.
The data shown in the list box in Figure 3.6 represents the value and
internal
representation of a single variant that is assigned a series of different
types. It's important to understand that Variant can't simultaneously
represent all types, and can instead at any one moment take on the
characteristics of only one particular type.
The code that assigns different types to a variant is easy to understand, as
is shown here:
void __fastcall TForm1::bVariantPlayClick(TObject
*Sender)
{
Variant V;
ShowVariant(V);
V = Null;
ShowVariant(V);
V = 1;
ShowVariant(V);
V = "Sam";
ShowVariant(V);
V = 1.25;
ShowVariant(V);
}
The code that reports on the current type of a variant is somewhat more
complex, but still relatively straightforward, as shown next:
AnsiString GetVariantType(Variant &V)
{
TVarData VarData;
AnsiString S;
VarData = TVarData(V);
switch (VarData.VType)
{
case varEmpty: S = "varEmpty"; break;
case varNull: S = "varNull"; break;
case varSmallint: S = "varSmallInt"; break;
case varInteger: S = "varInteger"; break;
case varSingle: S = "varSingle"; break;
case varDouble: S= "varDouble"; break;
case varCurrency: S = "varCurrency"; break;
case varDate: S = "varDate"; break;
case varOleStr: S = "varOleStr"; break;
case varDispatch: S = "varDispatch"; break;
case varError: S = "varError"; break;
case varBoolean: S = "varBoolean"; break;
case varVariant: S = "varVariant"; break;
case varUnknown: S = "varUnknown"; break;
case varString: S = "varString"; break;
case varTypeMask: S = "varTypeMask"; break;
case varByRef: S = "varByRef"; break;
case varByte: S = "varByte"; break;
case varArray: S = "varArray"; break;
default:
S = "Error";
}
return S;
}
This code first converts a variant into a variable of type TVarData.
In doing so, it is merely surfacing the true underlying type of the variant.
However, a variable of type TVarData will not act the same as a
variable of type Variant. This is because the compiler provides special
services for variants that it would not provide for a simple record type such as
TVarData.
It's important to note that there are at least two ways to write the first
lines of code in this function. For instance, I could have written
AnsiString GetVariantType(Variant &V)
{
AnsiString S;
switch (VarType(V))
{
case varEmpty: S = "varEmpty"; break;
case varNull: S = "varNull"; break;
// Code omitted
}
}
This code works the same as the code shown in the actual program found on the
CD-ROM.
However you decide to implement the function, the key point is that variants
can take on the appearance of being of a certain type. The chameleon-like
behavior of
Variant is sparked by the type of variable to which it is assigned.
If you want, you can think of a variant as a chameleon that hides itself from
view by assuming the coloration of the variable to which it is assigned. A
variant is never of type Variant; it's always either empty, NULL,
or the type of the variable to which it is assigned. In the same way, a
chameleon has no color of its own but is always changing to adapt its color to
the environment around it. Either that, or it is unborn, dead, nonexistent, or
has no color at all!
The following routines can all be used with variants. To learn more about
these routines, look in the online help.
extern void __fastcall VarClear(Variant &V);
extern void __fastcall VarCopy(Variant &Dest, const Variant &Source);
extern void __fastcall VarCopyNoInd(Variant &Dest, const Variant &Source);
extern void __fastcall VarCast(Variant &Dest, const Variant &Source, int
VarType);
extern int __fastcall VarType(const Variant &V);
extern Variant __fastcall VarAsType(const Variant &V, int VarType);
extern bool __fastcall VarIsEmpty(const Variant &V);
extern bool __fastcall VarIsNull(const Variant &V);
extern AnsiString __fastcall VarToStr(const Variant &V);
extern Variant __fastcall VarFromDateTime(TDateTime DateTime);
extern TDateTime __fastcall VarToDateTime(const Variant &V);
Here are some routines to use with Variant arrays.
extern Variant __fastcall VarArrayCreate(const int *Bounds, const int
Bounds_Size,int VarType);
extern Variant __fastcall VarArrayOf(const Variant *Values, const int
Values_Size);
extern void __fastcall VarArrayRedim(Variant &A, int HighBound);
extern int __fastcall VarArrayDimCount(const Variant &A);
extern int __fastcall VarArrayLowBound(const Variant &A, int Dim);
extern int __fastcall VarArrayHighBound(const Variant &A, int Dim);
extern void * __fastcall VarArrayLock(const Variant &A);
extern void __fastcall VarArrayUnlock(const Variant &A);
extern Variant __fastcall VarArrayRef(const Variant &A);
extern bool __fastcall VarIsArray(const Variant &A);
Before closing this section, I want to make it clear that variants are not
meant to be used broadly in your program whenever you need to work with a
variable. I have no doubt that some people will in fact program that way, but I
want to emphasize that the developers didn't really want variants to be used
that way. This is mainly because they have some overhead associated with them
that can slow down the compiler. Some tricks performed by variants, such as
string manipulation, happen to be highly optimized. However, you should never
consider a variant to be as fast or efficient as a standard C type.
Variants are almost certainly best used in OLE and database applications. In
particular, variants were brought into the language because they play a role in
OLE automation. Furthermore, much of the structure and behavior of variants is
defined by the rules of OLE. These unusual types also prove to be useful in
database applications. As a rule, there is so much overhead involved in
referencing any value from a database that a little additional variant
manipulation code is not going to make a significant difference.
Working with Text Files
Some of the best features of C++Builder are not immediately obvious to you
when you first start working with the tool, or even after you have been working
with the tool for awhile. For instance, C++Builder has a built in type called a
TStringList that all but makes obsolete the concept of a text file.
This class is so easy to use, and so powerful, that there are few occasions when
you would want to open a text file using fopen. It's important to note
that manipulating text files is not really the central purpose of the
TStringList class; however, it performs this task so well that there is
little need to ever resort to using fopen, fclose, and other
similar commands.
Before looking at how you would use to TStringList to open a text
file, it might be helpful to point out the places in BCB where this class is
most often used, as well as a few other features of the class.
The TStringList class is a descendant of the abstract class
TStrings. There are no instances of the TString class in BCB,
because this is an abstract type. However, the TStringList,
TListBox->Items, TComboBox->Items,
TMemo->Lines, TRichEdit->Lines and TQuery->VCL
classes all descend directly from TStrings. This means you can assign
instances of these classes to one another.
You can, for instance, create an instance of the TStringList class
and assign it to a TListBox->Items object:
TStringList *MyStringList = new TStringList;
MyStringList->Add("Some text");
MyStringList->Add("More text");
ListBox1->Items = MyStringList;
If you write code like this, ListBox1 will end up showing two items
in it, with the first being "Some text", and the second being "More
text".
You can write the contents of any string list to file by using the
SaveToFile method, and you can read the list back using LoadFromFile.
There are also SaveToStream and LoadFromStream methods:
MyStringList->SaveToFile("C:\\Sam.txt");
MyStringList->LoadFromFile("C:\\Sam.txt");
TStringLists have a sort property that can enable you to sort them
instantly. You can also associate an object with each string in the
TStringList using the InsertObject method.
Given the presence of all this flexibility in one simple-to-use object, it
does not really make sense to use any other tool for handling lists of strings.
It is definitely worth taking time to explore this object and to see what it can
do for you.
New Compiler Switches
I will spend little time in this book talking about how to pass switches to
the compiler. That subject simply is of no interest to programmers whose primary
goal is rapid application development. However, even under ideal conditions,
there may be times when a programmer may have to take off their RAD hat and put
on their Systems programming hat. So, if you absolutely need to start tweaking
the compiler, here are some new BCC32.EXE switches culled from the
online help:
New BCC32.EXE switches:
- -Vx switch for truly empty (0 length) structs
- -He switch (extern types in .OBJs)
- -Hs switch (smart-cached headers)
New BCC32.EXE pragmas:
- #pragma link "obj" (object file dependencies)
- #pragma resource "res" (resource file dependencies)
- #pragma anon_struct on (for support of VCL variant
records)
New switches for the Pascal DCC32.EXE compiler:
- -jp switch: creates Borland C++ compatible .OBJ files
- -jph switch: creates C++Builder compatible header (.HPP)
files from Object Pascal unit files (.DCL)
- -jphn switch: uses the Object Pascal unit name as the enclosing
C++ namespace for both .OBJs and .HPPs that are generated
- -n switch: specify .DCU output directory
- -nh switch: specify .HPP output directory
- -no switch: specify .OBJ output directory
Tips On Using the Visual Tools
One of the biggest problems with BCB forms is getting them to look correct in
all the possible resolutions. There is probably no surefire method for achieving
these ends. However, there are some tips that I have found that help.
When I build forms I almost always set the AutoScroll property to
False. If you do not do this, your form will always stay the same size,
which means that the edges of your form will be obscured in some resolutions. To
avoid this, you would have to resize the form to fit around the edges of your
controls when you changed resolutions.
Whatever approach you take, you simply have to view your form in at least
three or four different modes before releasing it to the public. In particular,
be sure to switch back and forth between Large Fonts and Small Fonts when you
change resolutions. If you are a complete perfectionist, you may decide that the
only solution is to have two sets of forms, one for use with Small Fonts, and
the other for use with Large Fonts.
Some people have a definite knack for designing good-looking forms. If you
find someone who can do this, ask them to critique your forms or even to help
you design them. If you find forms that you like, study them. Use whatever
tricks you can find
to help you create decent-looking forms. Try to leave some whitespace on your
forms if you can, and make sure controls line up vertically and horizontally
whenever possible.
Summary
In this chapter you saw an overview of all the key areas in which BCB and the
VCL meet. This is a complicated, yet extremely important aspect of the product.
I'm sure it tried your patience at times to have to go through so much dry
material. However, if you have this subject firmly under your belt, you are
ready to begin a serious study of BCB. You now know a good deal about how this
product is put together, and why it was constructed in this particular fashion.
This is the kind of knowledge for which there is no substitute. You simply have
to know these kinds of things about a product before you can take advantage of
its best features.
BCB is a tool for using components and objects to make applications. Any type
of application you can conceive of creating can be made better in Borland
C++Builder than it can with any other C/C++ compiler. What do I mean by better?
I mean that it can be put together faster, with a better chance of having the
right design, and with fewer bugs. In order to get that power, BCB had to tap
into the VCL. In this chapter you saw how it was done.
One of the most important themes of this chapter was the central role played
by components, properties, and events in the BCB programming model. This product
does
a lot of great things, but the key features that make it all possible are the
support for components, properties, and events. Much of the groundwork for that
support was laid out in this chapter.
|