Overview
This chapter is the first of a "two-part series" on constructing real-world
databases. The goal is to move from the largely theoretical information you got
in the preceding chapter into a few examples of how to make programs that
someone could actually use for a practical purpose in the real world.
In several sections of this chapter, I go into considerable depth about
design-related issues. One of the burdens of this chapter is not merely to show
how database code works, but to talk about how to create programs that have some
viable use in the real world. These design-related issues are among the most
important that any programmer will ever face.
In this chapter, you will get a look at a simple, nearly pure, flat-file
address book program called Address2. This program is designed to represent the
simplest possible database program that is still usable in a real-world
situation. In the next chapter, I will create a second address program, designed
to be a "killer" relational database with much more power than the one you see
in this chapter.
One of my primary goals in these two chapters is to lay out in the starkest
possible terms the key differences between flat-file and relational databases.
The point is for you to examine two database tools that perform the same task
and see exactly what the relational tool brings to the table. I found, however,
that it was simply impossible for me to create a completely flat-file database
design. As a result, I had to content myself with a design that was nearly a
pure example of a flat-file database. It does, however, contain one smaller
helper table that is linked in using relational database design principles. My
simple inability to omit this table does more than anything else I can say to
stress the weaknesses of the flat-file model, and to show why relational
databases are essential.
The second database program you see will contain a very powerful set of
relational features that could, with the aid of a polished interface, stand up
under the strain of heavy and complex demands. You could give this second
program to a corporate secretary or executive, and that person could make some
real use of it. The database shown in this chapter, on the other hand, is meant
to be a quick solution to a simple problem. One of the points you shouldn't
miss, however, is that the database from this chapter more than suits the needs
of most people.
One of the classic and most commonly made mistakes is to give people too many
features, or to concentrate on the wrong set of features. Those of us who work
in the industry forget how little experience most users have with computers.
Even the simple database program outlined in this chapter might be too much for
some people. Any attempt to sell them on the merits of the database from the
next chapter would simply be an exercise in futility. They would never be
willing to take the time to figure out what to do with it. As a result, I
suggest that you not turn your nose up at the database shown in this chapter
just because it is not as powerful as the one in the next chapter. Just because
your typical Volkswagen is not as powerful as an Alpha Romeo does not mean that
the Volkswagen people are in a small business niche, or even that there is less
money in VWs than in Alpha Romeos.
Here is a quick look at the terrain covered in this chapter:
- Sorting data.
¡¡
- Filtering data.
¡¡
- Searching for data.
¡¡
- Dynamically moving a table in and out of a read-only state.
¡¡
- Forcing the user to select a field's value from a list of valid responses.
¡¡
- Allowing the user to choose the colors of a form at runtime.
¡¡
- Saving information to the Registry. In particular, you see how to use the
Registry to replace an INI file, and how to save and restore information from
and to the Registry at program startup.
¡¡
- Using events that occur in a TDataModule inside the main form of
your program. That is, the chapter shows how to respond to events specific to
one form from inside a second form. Or, more generally, it shows how to handle
events manually rather than let BCB set up the event handler for you.
After finishing this chapter, you will have learned something about the kinds
of problems experienced when writing even a very basic database program that
serves a real-world purpose. The final product, though not quite up to
professional standards, provides solutions to many of the major problems faced
by programmers who want to create tools that can be used by the typical user. In
particular, the program explores how to use BCB to create a reasonably usable
interface.
You will find that the final program is relatively long when compared to most
of the programs you have seen so far in this book. The length of the program is
a result of my aspiration to make it useful in a real-world setting, while
simultaneously providing at least a minimum degree of robustness. The act of
adding a few niceties to the interface for a program gives you a chance to see
how RAD programming can help solve some fairly difficult problems.
Before closing this overview, I should perhaps explicitly mention that this
chapter does not cover printing, which is certainly one of the most essential
real-world needs for a database program. I will, however, add printing
capabilities to this program in Chapter 17, "Printing: QuickReport and Related
Technologies." In fact, that chapter will show how to add printing to all the
useful database programs that will be created in the next few chapters of this
book. My plan is to isolate the important, even crucial, subject of printing in
its own chapter where it can be properly addressed.
You should also be sure you have read the readme files on the CD that
accompanies this book for information about the alias used in the Address2
program and in other programs in this book. If you have trouble getting any of
these programs running, be sure to check my Web site (users.aol.com/charliecal)
for possible updates.
Defining the Data
When you're considering an address program, you can easily come up with a
preliminary list of needed fields:
First Name
Last Name
Address
City
State
Zip
Phone
After making this list and contemplating it for a moment, you might ask the
following questions:
- What about complex addresses that can't be written on one line?
¡¡
- Is one phone number enough? What about times when I need a home phone and
a work phone?
¡¡
- Speaking of work, what about specifying the name of the company that
employs someone on the list?
¡¡
- What about faxes?
¡¡
- This is the 1990s, so what about an e-mail address?
¡¡
- What about generic information that doesn't fit into any of these
categories?
This list of questions emerges only after a period of gestation. In a
real-world situation, you might come up with a list of questions like this only
after you talk with potential users of your program, after viewing similar
programs that are on the market, and after experimenting with a prototype of the
proposed program. Further information might be culled from your own experience
using or writing similar programs. Whatever way you come up with the proper
questions, the key point is that you spend the time to really think about the
kind of data you need.
- ¡¡
¡¡
NOTE: Many books tell you to
complete your plan before you begin programming. The only thing wrong with
this theory is that I have never seen it work out as expected in practice.
Nearly all the real-world programs that I have seen, both my own and others,
whether produced by individuals or huge companies, always seem to go through
initial phases that are later abandoned in favor of more sophisticated
designs. This is part of what Delphi and RAD programs in general are all
about. They make it possible for you to create a draft of your program and
then rewrite it.
Think hard about what you want to do. Then get up a prototype in fairly short
order, critique it, and then rethink your design. Totally abandoning the first
draft is rarely necessary, but you are almost certainly going to have to
rewrite. For this reason, concentrating on details at first is not a good
idea. Get things up and running; then if they look okay, go back and optimize.
The point is that the process is iterative. You keep rewriting, over and over,
the same way authors keep rewriting the chapters in their books. RAD
programming tools help make this kind of cycle possible. The interesting thing
about Delphi is that the same tool that lets you prototype quickly is also the
tool that lets you optimize down to the last clock cycle.
I don't, however, think most contemporary application programming is really
about optimization any more than it's about attempting to design a program
correctly on paper before writing it. My experience leads me to believe that
the practical plan that really works is iterative programming. Think for a
little bit and then write some code. Review it, then rewrite it, then review
it, and then rewrite it, and so on. Another, somewhat more old-fashioned name
for this process is simply: One heck of a lot of hard work!
After considering the preceding questions, you might come up with a revised
list of fields for your program:
First Name
Last Name
Company
Address1
Address2
City
State
Zip
Home Phone
Work Phone
Fax
EMail1
EMail2
Comment
This list might actually stand up to the needs of a real-world user.
Certainly, it doesn't cover all possible situations, but it does represent a
reasonable compromise between the desire to make the program easy to use and the
desire to handle a variety of potential user demands.
At this stage, you might start thinking about some of the basic functionality
you want to associate with the program. For example, you might decide that a
user of the program should be able to search, sort, filter, and print the data.
After stating these needs, you'll find that the user will need to break up the
data into various categories so that it can be filtered. The question, of
course, is how these categories can be defined.
After considering the matter for some time, you might decide that two more
fields should be added to the list. The first field can be called Category;
it holds a name that describes the type of record currently being viewed. For
example, some entries in an address book might consist of family members,
whereas other entries might reference friends, associates from work, companies
where you shop, or other types of data. A second field can be called Marked;
it designates whether a particular field is marked for some special processing.
Here is the revised list, with one additional field called Category,
that is used to help the user filter the data he or she might be viewing:
First Name
Last Name
Company
Address1
Address2
City
State
Zip
Home Phone
Work Phone
Fax
EMail1
EMail2
Comment
Category
Marked
After you carefully consider the fields that might be used in the Address2
program, the next step is to decide how large and what type the fields should
be. Table 13.1 shows proposed types and sizes.
Table 13.1. The lengths and types of fields used by the Address2 program.
| Name |
Type |
Size |
| FName |
Character |
40 |
| LName |
Character |
40 |
| Company |
Character |
40 |
| Address1 |
Character |
40 |
| Address2 |
Character |
40 |
| City |
Character |
40 |
| State |
Character |
5 |
| Zip |
Character |
15 |
| HPhone |
Character |
15 |
| WPhone |
Character |
15 |
| Fax |
Character |
15 |
| EMail1 |
Character |
45 |
| EMail2 |
Character |
45 |
| Comment |
Memo |
20 |
| Category |
Character |
15 |
| Marked |
Logical |
¡¡ |
As you can see, I prefer to give myself plenty of room in all the fields I
declare. In particular, notice that I have opted for wide EMail fields
to hold long Internet addresses, and I have decided to make the Comment
field into a memo field so that it can contain long entries, if necessary. The
names of some of the fields have also been altered so that they don't contain
any spaces. This feature might prove useful if the data is ever ported to
another database.
¡¡
Now that you have decided on the basic structure of the table, the next task
is to work out some of the major design issues. In particular, the following
considerations are important:
- The program should run off local tables, because this is the kind of tool
likely to be used on individual PCs rather than on a network. The choice of
whether to use Paradox or dBASE tables is a toss-up, but I'll opt to use
Paradox tables because they provide more features.
¡¡
- The user should be able to sort the table on the FName, LName,
and Company fields.
¡¡
- Searching on the FName, LName, and Company
fields should be possible.
¡¡
- The user should be able to set up filters based on the Category
field.
¡¡
- The times when the table is editable should be absolutely clear, and the
user should be able to easily move in and out of read-only mode.
¡¡
- Printing the contents of the table based on the filters set up by the
Category field should be possible.
¡¡
- Choosing a set of colors that will satisfy all tastes is very difficult,
so the user should be able to set the colors of the main features in the
program.
A brief consideration of the design decisions makes it clear that the table
should have a primary index on the first three fields and secondary indexes on
the FName, LName, Company, and Category
fields. The primary index can be used in place of a secondary index on the
FName field, but the intent of the program's code will be clearer if a
secondary index is used for this purpose. In other words, the code will be
easier to read if it explicitly sets the IndexName to something called
FNameIndex instead of simply defaulting to the primary index. Table
13.2 shows the final structure of the table. The three asterisks in the fourth
column of the table show the fields that are part of the primary index.
- ¡¡
¡¡
NOTE: This table does not have a
code field--that is, it does not have a simple numerical number in the first
field of the primary index. Most tables will have such a value, but it is not
necessary here, because this database is, at least in theory, a flat-file
database. I say, "at least in theory," because I am going to make one small
cheat in the structure of this database. In short, there will be a second
table involved, simply because I could see no reasonable way to omit it from
the design of this program.
Table 13.2. The fields used by the Address2 program.
| Name |
Type |
Size |
PIdx |
Index |
| FName |
Character |
40 |
* |
FNameIndex |
| LName |
Character |
40 |
* |
LNameIndex |
| Company |
Character |
40 |
* |
CompanyIndex |
| Address1 |
Character |
40 |
¡¡ |
¡¡ |
| Address2 |
Character |
40 |
¡¡ |
¡¡ |
| City |
Character |
40 |
¡¡ |
¡¡ |
| State |
Character |
5 |
¡¡ |
¡¡ |
| Zip |
Character |
15 |
¡¡ |
¡¡ |
| HPhone |
Character |
15 |
¡¡ |
¡¡ |
| WPhone |
Character |
15 |
¡¡ |
¡¡ |
| Fax |
Character |
15 |
¡¡ |
¡¡ |
| EMail1 |
Character |
45 |
¡¡ |
¡¡ |
| EMail2 |
Character |
45 |
¡¡ |
¡¡ |
| Comment |
Memo |
20 |
¡¡ |
¡¡ |
| Category |
Character |
15 |
¡¡ |
CategoryIndex |
| Marked |
Logical |
¡¡ |
¡¡ |
¡¡ |
Now that you have a clear picture of the type of table that you need to create,
you can open the Database Desktop and create the table, its primary index, and
its four secondary indexes. When you're done, the structure of the table should
look like that in Figure 13.1. You can save the table under the name
ADDRESS.DB.
FIGURE 13.1.
Designing the main table for the Address2 program. Portions of the table are not
visible in this picture.
¡¡
Here is another way of looking at the indexes for this table:
Primary Index
LName
FName
Company
Category Index
Category
Company Index
Company
FName
LName
LName Index
LName
FName
Company
In this particular case, I will actually end up using these fields and
indexes as designed. However, in a real-world situation, you should expect to
come up with a carefully thought-out draft like this, and then know in your
heart that after you get the program up and running, some things will have to
change. Don't tell someone: "Oh, I can complete this program in two weeks; this
is going to be easy!" Instead, say: "In two weeks, I can get you a prototype,
and then we can sit down and decide what changes need to be made."
You should, however, have some clearly defined boundaries. For example, this
program is designed to be a flat-file database. If someone (yourself most
especially included!) tries to talk you into believing that this program should
really be a relational database of the kind planned for the next chapter, then
you have to slam your foot down and say: "No way!" After you've started on a
project, you should expect revisions, but you must not allow the goal of the
project to be redefined. That way leads to madness!
Defining the Programs Appearance
Before beginning the real programming chores, you need to create a main form
and at least one of the several utility forms that will be used by the program.
You can let the Database Expert perform at least part of this task for you, but
I prefer to do the chore myself to give my program some individuality.
The main form of the Address2 program, shown in Figure 13.2, contains two
panels. On the top panel are all the labels and data-aware controls necessary to
handle basic input and output chores. All the main fields in the program can be
encapsulated in TDBEdit controls, except for the Comment
field, which needs a TDBMemo, and the Category field, which
needs a TDBLookupComboBox. The names of the data-aware controls should
match the field with which they are associated, so the first TDBEdit
control is called FNameEdit; the second, LNameEdit; and so on.
The TDBLookupComboBox is therefore called CategoryCombo--and
the memo field, CommentMemo.
FIGURE 13.2.
The main form for the Address2 program.
- ¡¡
¡¡
NOTE: If you find yourself
chafing under the restraints of my naming conventions, you shouldn't hesitate
to adopt the method you think best. For example, if you really prefer
eFName or plain FName rather than FNameEdit as the name
of a TDBEdit control, then you should go with your gut instinct.
My history in this regard is simple. I started out deploring Hungarian
notation and then slowly inched over to the point at which I was reluctantly
starting to use it in my programs.
Then there came a day when I was squinting at some egregious variable name
dreamed up by an undoubtedly besotted Microsoft employee, and I just knew that
I had had enough of abbreviations, and especially of prefixing them to a
variable name.
One of my original goals was to keep variable names short. I went to great
lengths to achieve this end. Then I watched C++ linkers mangle my short
variable names into behemoths that consumed memory like sharks possessed by a
feeding frenzy. After contemplating this situation for a while, I decided that
the one thing I could bring to the table that I really cared about was
clarity. As a result, I dropped Hungarian notation from all my new code and
began using whole words whenever possible.
The bottom panel should contain four buttons for navigating through the
table's records, as well as Edit, Insert, and Cancel buttons. A status bar at
the bottom of the main form provides room for optionally reporting on the
current status of the program.
The top of the program contains a menu with the following format:
Caption = `File'
Caption = `Print'
Caption = `-'
Caption = `Exit'
Caption = `Edit'
Caption = `Copy'
Caption = `Cut'
Caption = `Paste'
Caption = `Options'
Caption = `Filter'
Caption = `Set Category'
Caption = `Search'
Caption = `First Name'
Caption = `Last Name'
Caption = `Company'
Caption = `Sorts'
Caption = `First Name'
Caption = `Last Name'
Caption = `Company'
Caption = `Colors'
Caption = `Form'
Caption = `Edits'
Caption = `Edit Text'
Caption = `Labels'
Caption = `------'
Caption = `Panels'
Caption = `System'
Caption = `Default'
Caption = `------'
Caption = `The Blues'
Caption = `Save Colors'
Caption = `Read Colors'
Caption = `Marks'
Caption = `Mark All'
Caption = `Clear All Marks'
Caption = `Print Marked to File'
Caption = `Show Only Marked'
Caption = `Help'
Caption = `About'
Each line represents the caption for one entry in the program's main menu.
The indented portions are the contents of the drop-down menus that appear when
you select one of the menu items visible in Figure 13.2.
After you create the program's interface, drop down a TTable and
TDataSource on a data module, wire them up to ADDRESS.DB, and hook
up the fields to the appropriate data-aware control. Name the TTable
object AddressTable and name the TDataSource object
AddressSource. To make this work correctly, you should create an alias,
called Address, that points to the location of ADDRESS.DB.
Alternatively, you can create a single alias called CUnleashed that
points to the tables that ship on the CD that accompanies this book. Take a look
at the readme files on the CD that accompanies this book for further information
on aliases.
Now switch back to the main form, use the File | Include Unit Header option
to connect the main form and the TDataModule, and hook up the
data-aware controls shown in Figure 13.2 to the fields in the address table. The
only tricky part of this process involves the Category field, which is
connected to the TDBLookupComboBox. I will explain how to use this
field over the course of the next few paragraphs.
If you run the program you have created so far, you will find that the
TDBLookupComboBox for the Category field does not contain any
entries; that is, you can't drop down its list. The purpose of this control is
to enable the user to select categories from a prepared list rather than force
the user to make up categories on the fly. The list is needed to prevent users
from accidentally creating a whole series of different names for the same
general purpose.
Consider a case in which you want to set a filter for the program that shows
only a list of your friends. To get started, you should create a category called
Friend and assign it to all the members of the list that fit that
description. If you always choose this category from a drop-down list, it will
presumably always be spelled the same. However, if you rely on users to type
this word, you might get a series of related entries that look like this:
Friend
Friends
Frends
Acquaintances
Buddies
Buds
Homies
HomeBoys
Amigos
Chums
Cronies
Companions
This mishmash of spellings and synonyms won't do you any good when you want
to search for the group of records that fits into the category called Friend.
The simplest way to make this happen is to use not a TDBLookupComboBox,
but a TDBLookupCombo. To use this control, simply pop open the Property
Editor for the Items property and type in a list of categories such as
the following:
Home
Work
Family
Local Business
Friend
Now when you run the program and drop down the Category combo box, you will
find that it contains the preceding list.
The only problem with typing names directly into the Items property
for the TDBLookupCombo is that changing this list at runtime is
impractical. To do away with this difficulty, the program stores the list in a
separate table, called CATS.DB. This table has a single character field
that is 20 characters wide. After creating the table in the Database Desktop,
you can enter the following five strings into five separate records:
Home
Work
Family
Local Business
Friend
Now that you have two tables, it's best to switch away from the
TDBLookupCombo and go instead with the TDBLookupComboBox. You make
the basic connection to the TDBLookupComboBox by setting its
DataSource field to AddressSource and its DataField to
Category. Then set the ListSource for the control to
CatSource, and set ListField and KeyField to Category.
- ¡¡
¡¡
NOTE: The addition of the
TDBLookupComboBox into the program begs the question of whether
Address2 is really a flat-file database because lookups at least give the
feel commonly associated with relational databases. The lookup described in
the preceding few paragraphs is not, however, a pure relational technique, in
that the CATS and Address tables are not bound by a primary
and a foreign key.
It is, however, a cheat in the design of the program, since my goal was to
create a pure flat-file database. The facts here are simple: I want the
database to be as simple as possible, but I also want it to be useful. Without
this one feature, I saw the program as hopelessly crippled. As stated earlier,
this shows the importance of relational database concepts in even the simplest
programs. In short, I don't think I can get any work done without using
relational techniques. Relational database design is not a nicety; it's a
necessity.
To allow the user to change the contents of the CATS table, you can
create a form like the one shown in Figure 13.3. This form needs only minimal
functionality because discouraging the user from changing the list except when
absolutely necessary is best. Note that you need to add the CategoryDlg
module's header to the list of files included in the main form. You can do so
simply by choosing File | Include Unit Header.
FIGURE 13.3.
The Category form enables the user to alter the contents of CATS.DB.
At program startup, the Category dialog, and the memory associated with it,
does not need to be created and allocated. As a result, you should choose
Options | Project, select the Forms page, and move the Category dialog into the
Available Forms column. In response to a selection of the Set Category menu item
from the main form of the Address2 program, you can write the following code:
void __fastcall TForm1::Category1Click(TObject *Sender)
{
CategoryDlg = new TCategoryDlg(this);
CategoryDlg->ShowModal();
CategoryDlg->Free();
}
This code creates the Category dialog, shows it to the user, and finally
deallocates its memory after the user is done. You can take this approach
because it assures that the Category dialog is in memory only when absolutely
necessary.
Setting Up the Command Structure for the Program
The skeletal structure of the Address2 program is starting to come together.
However, you must complete one remaining task before the core of the program is
complete. A number of basic commands are issued by the program, and they can be
defined in a single enumerated type:
enum TCommandType {ctClose, ctInsert, ctPrior,
ctEdit, ctNext, ctCancel,
ctPrint, ctFirst, ctLast,
ctPrintPhone, ctPrintAddress,
ctPrintAll, ctDelete};
This type enables you to associate each of the program's commands with the
Tag field of the appropriate button or menu item, and then to associate
all these buttons or menu items with a single method that looks like this:
void __fastcall TForm1::CommandClick(TObject *Sender)
{
switch (dynamic_cast<TComponent*>(Sender)->Tag)
{
case ctClose: Close(); break;
case ctInsert: DMod->AddressTable->Insert(); break;
case ctPrior: DMod->AddressTable->Prior(); break;
case ctEdit: HandleEditMode(); break;
case ctNext: DMod->AddressTable->Next(); break;
case ctCancel: DMod->AddressTable->Cancel(); break;
case ctPrint: PrintData(ctPrint); break;
case ctFirst: DMod->AddressTable->First(); break;
case ctLast: DMod->AddressTable->Last(); break;
case ctPrintPhone: PrintData(ctPrintPhone); break;
case ctPrintAddress: PrintData(ctPrintAddress); break;
case ctPrintAll: PrintData(ctPrintAll); break;
case ctDelete:
AnsiString S = DMod->AddressTableLName->AsString;
if (MessageBox(Handle, "Delete?", S.c_str(), MB_YESNO) == ID_YES)
DMod->AddressTable->Delete();
break;
}
}
This code performs a simple typecast to allow you to access the Tag
field of the component that generated the command. This kind of typecast was
explained in depth in Chapter 4, "Events."
There is no reason why you can't have a different method associated with each
of the buttons and menu items in the program. However, handling things this way
is neater and simpler, and the code you create is much easier to read. The key
point here is to be sure that the Tag property of the appropriate
control gets the correct value and that all the controls listed here have the
OnClick method manually set to the CommandClick method. I took
all these steps while in design mode, being careful to associate the proper
value with the Tag property of each control.
Table 13.3 gives a brief summary of the commands passed to the
CommandClick method.
Table 13.3. Commands passed to CommandClick.
| Command |
Type |
Name |
Tag |
| Exit |
TMenuItem |
btClose |
0 |
| Insert |
TButton |
btInsert |
1 |
| Prior |
TButton |
btPrior |
2 |
| Edit |
TButton |
btEdit |
3 |
| Next |
TButton |
btNext |
4 |
| Cancel |
TButton |
btCancel |
5 |
| Print |
TMenuItem |
btPrint |
6 |
| First |
TButton |
btFirst |
7 |
| Last |
TButton |
btLast |
8 |
The task of filling in the Tag properties and setting the OnClick
events for all these controls is a bit tedious, but I like the easy-to-read code
produced by following this technique. In particular, I like having all the major
commands send to one method, thereby giving me a single point from which to
moderate the flow of the program. This is particularly useful when you can
handle most of the commands with a single line of code. Look, for example, at
the ctNext and ctCancel portions of the case
statement in the CommandClick method.
¡¡
All the code in this program will compile at this stage except for the
references in CommandClick to HandleEditMode and PrintData.
For now, you can simply create dummy HandleEditMode and PrintData
private methods and leave their contents blank.
At this stage, you're ready to run the Address2 program. You can now insert
new data, iterate through the records you create, cancel accidental changes, and
shut down the program from the menu. These capabilities create the bare
functionality needed to run the program.
Examining the "Rough Draft" of an Application
The program as it exists now is what I mean by creating a "rough draft" of a
program. The rough draft gets the raw functionality of the program up and
running with minimum fuss, and it lets you take a look at the program to see if
it passes muster.
If you were working for a third-party client, or for a demanding boss, now
would be the time to call the person or persons in question and have them
critique your work.
"Is this what you're looking for?" you might ask. "Do you think any fields
need to be there that aren't yet visible? Do you feel that the project is headed
in the right direction?"
Nine times out of ten, these people will come back to you with a slew of
suggestions, most of which have never occurred to you. If they have
irreconcilable differences of opinion about the project, now is the time to find
out. If they have some good ideas you never considered, now is the time to add
them.
Now you also have your chance to let everyone know that after this point
making major design changes may become impossible. Let everyone know that you're
about to start doing the kind of detail work that is very hard to undo. If
people need a day or two to think about your proposed design, give it to them.
Making changes now, at the start, is better than after you have everything
polished and spit-shined. By presenting people with a prototype, you give them a
sense of participating in the project, which at least potentially puts them on
your side when you turn in the finished project.
To help illustrate the purpose of this portion of the project development, I
have waited until this time to point out that it might be helpful to add a grid
to the program so that the user can see a list of names from which to make a
selection. This kind of option may make no sense if you're working with huge
datasets, but if you have only a few hundred or a few thousand records, then a
grid can be useful. (The TDBGrid is powerful enough to display huge
datasets, but there is a reasonable debate over whether grids are the right
interface element for tables that contain hundreds of thousands or millions of
records.)
When using the grid, you have to choose which fields will be shown in it. If
you choose the last name field, then you have a problem for records that include
only the company name, and if you use the company name, then the reverse problem
kicks in. To solve this dilemma, I create a calculated field called
FirstLastCompany that looks like this:
void __fastcall TDMod::AddressTableCalcFields(TDataSet *DataSet)
{
if ((!AddressTableFName->IsNull) || (!AddressTableLName->IsNull))
AddressTableFirstLast->Value =
AddressTableFName->Value + " " + AddressTableLName->Value;
else if (!AddressTableCompany->IsNull)
AddressTableFirstLast->Value = AddressTableCompany->Value;
else
AddressTableFirstLast->Value = "Blank Record";
}
The code specifies that if a first or last name appears in the record, then
that name should be used to fill in the value for the calculated field. However,
if they are both blank, then the program will supply the company name instead.
As an afterthought, I decided that if all three fields are blank, then the
string "Blank Record" should appear in the calculated field.
I hope that you can now see why I feel that optimization issues should always
be put off until the interface, design, and basic coding of the program are
taken through at least one draft. It would be foolish to spend days or weeks
optimizing routines that you, or a client, ultimately do not believe are
necessary, or even wanted, in the final release version of the program. Get the
program up and running, and then, if everyone agrees that it looks right, you
can decide if it needs to be optimized or if you have time for optimization.
Program development is usually an iterative process, with a heavy focus on
design issues. I don't think that working on the assumption you'll get it right
the first time is wise.
Creating a Finished Program
The remaining portions of this chapter will tackle the issues that improve
this program to the point that it might be useful in a real-world situation. All
but the most obvious or irrelevant portions of the code for the Address2 program
are explained in detail in the remainder of this chapter.
Listings 13.1 through 13.7 show the code for the finished program. I discuss
most of this code in one place or another in this chapter. Once again, the goal
of this program is to show you something that is reasonably close to being
useful in a real-world situation. The gap between the sketchy outline of a
program, as discussed earlier, and a product that is actually usable forms the
heart of the discussion that follows. In fact, most of my discussion of
databases that you have read in the preceding chapters has concentrated on the
bare outlines of a real database program. You have to know those raw tools to be
able to write any kind of database program. However, they are not enough, and at
some point you have to start putting together something that might be useful to
actual human beings. (Remember them?) That sticky issue of dealing with human
beings, and their often indiscriminate foibles, forms the subtext for much of
what is said in the rest of this chapter.
Listing 13.1. The source code for the header of the
main form of the Address2 program.
///////////////////////////////////////
// File: Main.h
// Project: Address2
// 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>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\DBCtrls.hpp>
#include <vcl\Mask.hpp>
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
#include <vcl\Menus.hpp>
#include <vcl\Dialogs.hpp>
#include <vcl\Report.hpp>
#include <vcl\ComCtrls.hpp>
#include <vcl\DBGrids.hpp>
#include <vcl\Grids.hpp>
#define READ_ONLY_STRING " [Read Only Mode]"
#define EDIT_MODE_STRING " [Edit Mode]"
enum TSearchSortType {stFirst, stLast, stCompany};
enum TColorType {ccForm, ccEdit, ccEditText, ccLabel, ccPanel};
enum TChangeType {tcColor, tcFontColor};
enum TCommandType {ctClose, ctInsert, ctPrior,
ctEdit, ctNext, ctCancel,
ctPrint, ctFirst, ctLast,
ctPrintPhone, ctPrintAddress,
ctPrintAll, ctDelete};
class TForm1 : public TForm
{
__published: // IDE-managed Components
TPanel *Panel2;
TButton *InsertBtn;
TButton *EditBtn;
TButton *CancelBtn;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *PrintAddresses1;
TMenuItem *PrintPhoneOnly1;
TMenuItem *PrintEverything1;
TMenuItem *Print1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *Edit1;
TMenuItem *Copy1;
TMenuItem *Cut1;
TMenuItem *Paste1;
TMenuItem *Options1;
TMenuItem *Search1;
TMenuItem *Filter1;
TMenuItem *Category1;
TMenuItem *Sorts1;
TMenuItem *FirstName1;
TMenuItem *LastName1;
TMenuItem *Company1;
TMenuItem *Colors1;
TMenuItem *FormColor1;
TMenuItem *EditColor1;
TMenuItem *EditText1;
TMenuItem *Labels1;
TMenuItem *Panels1;
TMenuItem *Marks1;
TMenuItem *MarkAll1;
TMenuItem *ClearAllMarks1;
TMenuItem *PrintMarkedtoFile1;
TMenuItem *Help1;
TMenuItem *About1;
TColorDialog *ColorDialog1;
TDBNavigator *DBNavigator1;
TStatusBar *StatusBar1;
TBevel *Bevel1;
TPanel *Panel1;
TLabel *Label2;
TLabel *Label3;
TLabel *Address1;
TLabel *Address2;
TLabel *City;
TLabel *State;
TLabel *Zip;
TLabel *Company;
TLabel *HPhone;
TLabel *WPhone;
TLabel *Fax;
TLabel *Comment;
TLabel *EMail1;
TLabel *Category;
TLabel *EMail2;
TSpeedButton *SpeedButton1;
TDBEdit *LNameEdit;
TDBEdit *FNameEdit;
TDBEdit *Address1Edit;
TDBEdit *Address2Edit;
TDBEdit *CityEdit;
TDBEdit *StateEdit;
TDBEdit *ZipEdit;
TDBEdit *CompanyEdit;
TDBEdit *HomePhoneEdit;
TDBEdit *WorkPhoneEdit;
TDBEdit *FaxEdit;
TDBEdit *EMail1Edit;
TDBEdit *EMail2Edit;
TDBMemo *CommentMemo;
TDBLookupComboBox *CategoryCombo;
TButton *DeleteBtn;
TDBGrid *DBGrid1;
TMenuItem *FNameSearch;
TMenuItem *LNameSearch;
TMenuItem *CompanySearch;
TMenuItem *N3;
TMenuItem *System1;
TMenuItem *Defaults1;
TMenuItem *Blues1;
TMenuItem *N4;
TMenuItem *SaveCustom1;
TMenuItem *ReadCustom1;
TMenuItem *N2;
TMenuItem *ShowOnlyMarked1;
void __fastcall Copy1Click(TObject *Sender);
void __fastcall CommandClick(TObject *Sender);
void __fastcall AddressSourceStateChange(TObject *Sender);
void __fastcall FormShow(TObject *Sender);
void __fastcall About1Click(TObject *Sender);
void __fastcall CommandSortClick(TObject *Sender);
void __fastcall CommandSearchClick(TObject *Sender);
void __fastcall CommandColorClick(TObject *Sender);
void __fastcall System1Click(TObject *Sender);
void __fastcall Defaults1Click(TObject *Sender);
void __fastcall Blues1Click(TObject *Sender);
void __fastcall SaveCustom1Click(TObject *Sender);
void __fastcall ReadCustom1Click(TObject *Sender);
void __fastcall Filter1Click(TObject *Sender);
void __fastcall AddressSourceDataChange(TObject *Sender, TField *Field);
void __fastcall Category1Click(TObject *Sender);
void __fastcall MarkAll1Click(TObject *Sender);
void __fastcall ClearAllMarks1Click(TObject *Sender);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall ShowOnlyMarked1Click(TObject *Sender);
private: // User declarations
AnsiString FCaptionString;
void DoSort(TObject *Sender);
void HandleEditMode();
void SetReadOnly(BOOL NewState);
void PrintData(TCommandType Command);
void SetEdits(TColor Color);
void SetEditText(TColor Color);
void SetLabels(TColor Color);
void SetPanels(TColor Color);
TColor GetColor(TObject *Sender);
public: // User declarations
virtual __fastcall TForm1(TComponent* Owner);
};
//--------------------------------------------------------------------------
extern TForm1 *Form1;
//--------------------------------------------------------------------------
#endif
Listing 13.2. The main form for the Address2
program.
///////////////////////////////////////
// File: Main.cpp
// Project: Address2
// Copyright (c) 1997 by Charlie Calvert
#include <vcl\vcl.h>
#include <vcl\clipbrd.hpp>
#include <vcl\registry.hpp>
#pragma hdrstop
#include "Main.h"
#include "DMod1.h"
#include "AboutBox1.h"
#include "FileDlg1.h"
#include "Category1.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FCaptionString = Caption;
ReadCustom1Click(NULL);
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
SaveCustom1Click(NULL);
}
void TForm1::DoSort(TObject *Sender)
{
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case stFirst:
DMod->AddressTable->IndexName = "FNameIndex";
break;
case stLast:
DMod->AddressTable->IndexName = "LNameIndex";
break;
case stCompany:
DMod->AddressTable->IndexName = "CompanyIndex";
break;
}
}
void __fastcall TForm1::Copy1Click(TObject *Sender)
{
if (dynamic_cast<TDBEdit*>(ActiveControl))
(dynamic_cast<TDBEdit*>(ActiveControl))->CopyToClipboard();
if (dynamic_cast<TDBMemo*>(ActiveControl))
dynamic_cast<TDBMemo*>(ActiveControl)->CopyToClipboard();
if (dynamic_cast<TDBComboBox*>(ActiveControl))
Clipboard()->AsText = dynamic_cast<TDBComboBox*>(ActiveControl)->Text;
}
void __fastcall TForm1::CommandClick(TObject *Sender)
{
switch (dynamic_cast<TComponent*>(Sender)->Tag)
{
case ctClose: Close(); break;
case ctInsert: DMod->AddressTable->Insert(); break;
case ctPrior: DMod->AddressTable->Prior(); break;
case ctEdit: HandleEditMode(); break;
case ctNext: DMod->AddressTable->Next(); break;
case ctCancel: DMod->AddressTable->Cancel(); break;
case ctPrint: PrintData(ctPrint); break;
case ctFirst: DMod->AddressTable->First(); break;
case ctLast: DMod->AddressTable->Last(); break;
case ctPrintPhone: PrintData(ctPrintPhone); break;
case ctPrintAddress: PrintData(ctPrintAddress); break;
case ctPrintAll: PrintData(ctPrintAll); break;
case ctDelete:
AnsiString S = DMod->AddressTableLName->AsString;
if (MessageBox(Handle, "Delete?", S.c_str(), MB_YESNO) == ID_YES)
DMod->AddressTable->Delete();
break;
}
}
void TForm1::HandleEditMode()
{
InsertBtn->Enabled = !DMod->AddressSource->AutoEdit;
CancelBtn->Enabled = !DMod->AddressSource->AutoEdit;
DeleteBtn->Enabled = !DMod->AddressSource->AutoEdit;
if (!DMod->AddressSource->AutoEdit)
{
SetReadOnly(True);
EditBtn->Caption = "Stop Edit";
Caption = FCaptionString + EDIT_MODE_STRING;
}
else
{
if (DMod->AddressTable->State != dsBrowse)
DMod->AddressTable->Post();
SetReadOnly(False);
EditBtn->Caption = "Goto Edit";
Caption = FCaptionString + READ_ONLY_STRING;
}
}
void TForm1::PrintData(TCommandType Command)
{
}
void TForm1::SetReadOnly(BOOL NewState)
{
DMod->AddressSource->AutoEdit = NewState;
}
void __fastcall TForm1::AddressSourceStateChange(TObject *Sender)
{
AnsiString S;
switch (DMod->AddressTable->State)
{
case dsInactive:
S = "Inactive";
break;
case dsBrowse:
S = "Browse";
break;
case dsEdit:
S = "Edit";
break;
case dsInsert:
S = "Insert";
break;
case dsSetKey:
S = "SetKey";
break;
}
StatusBar1->SimpleText = "State: " + S;
}
void __fastcall TForm1::AddressSourceDataChange(TObject *Sender,
TField *Field)
{
HBITMAP BulbOn, BulbOff;
Caption = DMod->AddressTable->FieldByName("Marked")->AsString;
if (DMod->AddressTable->FieldByName("Marked")->AsBoolean)
{
BulbOn = LoadBitmap((HINSTANCE)HInstance, "BulbOn");
SpeedButton1->Glyph->Handle = BulbOn;
}
else
{
BulbOff = LoadBitmap((HINSTANCE)HInstance, "BulbOff");
SpeedButton1->Glyph->Handle = BulbOff;
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
DMod->AddressTable->Edit();
DMod->AddressTableMarked->AsBoolean = !DMod->AddressTableMarked->AsBoolean;
DMod->AddressTable->Post();
}
void __fastcall TForm1::FormShow(TObject *Sender)
{
DMod->AddressSource->OnStateChange = AddressSourceStateChange;
AddressSourceStateChange(NULL);
DMod->AddressSource->OnDataChange = AddressSourceDataChange;
AddressSourceDataChange(NULL, NULL);
}
void __fastcall TForm1::About1Click(TObject *Sender)
{
AboutBox->ShowModal();
}
void __fastcall TForm1::CommandSortClick(TObject *Sender)
{
DoSort(Sender);
DMod->AddressTable->FindNearest(OPENARRAY(TVarRec, ("AAAA")));
}
void __fastcall TForm1::CommandSearchClick(TObject *Sender)
{
AnsiString S;
if (InputQuery("Search Dialog", "Enter Name", S))
{
DoSort(Sender);
DMod->AddressTable->FindNearest(OPENARRAY(TVarRec, (S)));
}
}
TColor TForm1::GetColor(TObject *Sender)
{
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case ccForm:
return Form1->Color;
break;
case ccEdit:
return FNameEdit->Color;
break;
case ccEditText:
return FNameEdit->Font->Color;
break;
case ccLabel:
return Label2->Color;
break;
case ccPanel:
return Panel1->Color;
break;
}
}
void __fastcall TForm1::CommandColorClick(TObject *Sender)
{
ColorDialog1->Color = GetColor(Sender);
if (!ColorDialog1->Execute())
return;
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case ccForm:
Form1->Color = ColorDialog1->Color;
break;
case ccEdit:
SetEdits(ColorDialog1->Color);
break;
case ccEditText:
SetEditText(ColorDialog1->Color);
break;
case ccLabel:
SetLabels(ColorDialog1->Color);
break;
case ccPanel:
SetPanels(ColorDialog1->Color);
break;
}
}
void TForm1::SetEdits(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TDBEdit *>(Components[i]))
dynamic_cast<TDBEdit *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBGrid *>(Components[i]))
dynamic_cast<TDBGrid *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBMemo *>(Components[i]))
dynamic_cast<TDBMemo *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBLookupComboBox *>(Components[i]))
dynamic_cast<TDBLookupComboBox *>(Components[i])->Color = Color;
}
}
void TForm1::SetEditText(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TDBEdit *>(Components[i]))
dynamic_cast<TDBEdit *>(Components[i])->Font->Color = Color;
else if (dynamic_cast<TDBGrid *>(Components[i]))
dynamic_cast<TDBGrid *>(Components[i])->Font->Color = Color;
else if (dynamic_cast<TDBMemo *>(Components[i]))
dynamic_cast<TDBMemo *>(Components[i])->Font->Color = Color;
else if (dynamic_cast<TDBLookupComboBox *>(Components[i]))
dynamic_cast<TDBLookupComboBox *>(Components[i])->Font->Color = Color;
}
}
void TForm1::SetLabels(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TLabel *>(Components[i]))
dynamic_cast<TLabel *>(Components[i])->Font->Color = Color;
}
void TForm1::SetPanels(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TPanel *>(Components[i]))
dynamic_cast<TPanel *>(Components[i])->Color = Color;
}
void __fastcall TForm1::System1Click(TObject *Sender)
{
SetEdits(clWindow);
SetEditText(clBlack);
SetLabels(clBlack);
SetPanels(clBtnFace);
Form1->Color = clBtnFace;
}
void __fastcall TForm1::Defaults1Click(TObject *Sender)
{
SetEdits(clNavy);
SetEditText(clYellow);
SetLabels(clBlack);
SetPanels(clBtnFace);
Form1->Color = clBtnFace;
}
void __fastcall TForm1::Blues1Click(TObject *Sender)
{
SetEdits(0x00FF8080);
SetEditText(clBlack);
SetLabels(clBlack);
SetPanels(0x00FF0080);
Form1->Color = clBlue;
}
void __fastcall TForm1::SaveCustom1Click(TObject *Sender)
{
TRegIniFile *RegFile = new TRegIniFile("SOFTWARE\\Charlie's Stuff\\Address2");
RegFile->WriteInteger("Colors", "Form", Form1->Color);
RegFile->WriteInteger("Colors", "Edit Text", FNameEdit->Font->Color);
RegFile->WriteInteger("Colors", "Panels", Panel1->Color);
RegFile->WriteInteger("Colors", "Labels", Label2->Font->Color);
RegFile->WriteInteger("Colors", "Edits", FNameEdit->Color);
RegFile->Free();
}
void __fastcall TForm1::ReadCustom1Click(TObject *Sender)
{
TColor Color = RGB(0,0,255);
TRegIniFile *RegFile = new TRegIniFile("SOFTWARE\\Charlie's Stuff\\Address2");
Form1->Color = RegFile->ReadInteger("Colors", "Form", Color);
Color = RegFile->ReadInteger("Colors", "Edit Text", Color);
SetEditText(Color);
Color = RegFile->ReadInteger("Colors", "Panels", Color);
SetPanels(Color);
Color = RegFile->ReadInteger("Colors", "Labels", Color);
SetLabels(Color);
Color = RegFile->ReadInteger("Colors", "Edits", Color);
SetEdits(Color);
RegFile->Free();
}
void __fastcall TForm1::Filter1Click(TObject *Sender)
{
AnsiString S;
if (Filter1->Caption == "Filter")
{
if (FilterDlg->ShowModal() == mrOk)
{
S = DMod->CatsTableCATEGORY->Value;
if (S.Length() == 0)
return;
Filter1->Caption = "Cancel Filter";
DMod->AddressTable->IndexName = "CategoryIndex";
DMod->AddressTable->SetRange(OPENARRAY(TVarRec, (S)), OPENARRAY(TVarRec, (S)));
}
}
else
{
Filter1->Caption = "Filter";
DMod->AddressTable->CancelRange();
}
}
void __fastcall TForm1::Category1Click(TObject *Sender)
{
CategoryDlg = new TCategoryDlg(this);
CategoryDlg->ShowModal();
CategoryDlg->Free();
}
void __fastcall TForm1::MarkAll1Click(TObject *Sender)
{
DMod->ChangeMarked(True);
}
void __fastcall TForm1::ClearAllMarks1Click(TObject *Sender)
{
DMod->ChangeMarked(False);
}
void __fastcall TForm1::ShowOnlyMarked1Click(TObject *Sender)
{
ShowOnlyMarked1->Checked = !ShowOnlyMarked1->Checked;
DMod->AddressTable->Filtered = ShowOnlyMarked1->Checked;
}
Listing 13.3. The header for the TDataModule for the
Address2 program.
///////////////////////////////////////
// File: DMod1.h
// Project: Address2
// Copyright (c) 1997 by Charlie Calvert
#ifndef DMod1H
#define DMod1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
class TDMod : public TDataModule
{
__published: // IDE-managed Components
TTable *AddressTable;
TStringField *AddressTableFName;
TStringField *AddressTableLName;
TStringField *AddressTableCompany;
TStringField *AddressTableAddress1;
TStringField *AddressTableAddress2;
TStringField *AddressTableCity;
TStringField *AddressTableState;
TStringField *AddressTableZip;
TStringField *AddressTableCountry;
TStringField *AddressTableHPhone;
TStringField *AddressTableWPhone;
TStringField *AddressTableFax;
TStringField *AddressTableEMail1;
TStringField *AddressTableEMail2;
TStringField *AddressTableCategory;
TBooleanField *AddressTableMarked;
TStringField *AddressTableFirstLast;
TStringField *AddressTableCityStateZip;
TMemoField *AddressTableComment;
TDataSource *AddressSource;
TTable *CatsTable;
TDataSource *CatsSource;
TStringField *CatsTableCATEGORY;
TQuery *ChangeMarkedQuery;
void __fastcall AddressTableCalcFields(TDataSet *DataSet);
void __fastcall AddressTableFilterRecord(TDataSet *DataSet, bool &Accept);
private: // User declarations
public: // User declarations
virtual __fastcall TDMod(TComponent* Owner);
void ChangeMarked(BOOL NewValue);
};
extern TDMod *DMod;
#endif
Listing 13.4. The code for the data module of the
Address2 program.
///////////////////////////////////////
// File: DMod1.cpp
// Project: Address2
// Copyright (c) 1997 by Charlie Calvert
#include <vcl\vcl.h>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
AddressTable->Open();
CatsTable->Open();
}
void __fastcall TDMod::AddressTableCalcFields(TDataSet *DataSet)
{
if ((!AddressTableFName->IsNull) || (!AddressTableLName->IsNull))
AddressTableFirstLast->Value =
AddressTableFName->Value + " " + AddressTableLName->Value;
else if (!AddressTableCompany->IsNull)
AddressTableFirstLast->Value = AddressTableCompany->Value;
else
AddressTableFirstLast->Value = "Blank Record";
}
void TDMod::ChangeMarked(BOOL NewValue)
{
ChangeMarkedQuery->Close();
if (NewValue)
ChangeMarkedQuery->ParamByName("NewValue")->AsString = "T";
else
ChangeMarkedQuery->ParamByName("NewValue")->AsString = "F";
ChangeMarkedQuery->ExecSQL();
AddressTable->Refresh();
}
void __fastcall TDMod::AddressTableFilterRecord(TDataSet *DataSet,
bool &Accept)
{
Accept = (AddressTableMarked->AsBoolean == True);
}
Listing 13.5. The FilterDialog has very little code
in it.
///////////////////////////////////////
// File: FileDlg1.cpp
// Project: Address2
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "FileDlg1.h"
#include "DMod1.h"
#pragma resource "*.dfm"
TFilterDlg *FilterDlg;
__fastcall TFilterDlg::TFilterDlg(TComponent* Owner)
: TForm(Owner)
{
}
Listing 13.6. The Category dialog allows the user to
edit the list of categories.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Category1.h"
#include "DMod1.h"
#pragma resource "*.dfm"
TCategoryDlg *CategoryDlg;
__fastcall TCategoryDlg::TCategoryDlg(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TCategoryDlg::HelpBtnClick(TObject *Sender)
{
AnsiString S =
"'Twas brillig, and the slithy toves\r"
"Did gyre and gimble in the wabe;\r"
"All mimsy were the borogoves,\r"
"And the mome raths outgabe.\r"
"Beware the Jabberwock, my son!\r"
"The jaws that bite, the claws that catch!\r"
"Beware the Jubjub bird, and shun\r"
"The frumious Bandersnatch!\r"
"He took his vorpal sword in hand:\r"
"Long time the manxome foe he sought--\r"
"So rested he by the Tumtum tree,\r"
"And stood a while in thought\r"
"And as in uffish though he stood,\r"
"The Jabberwock, with eyes of flame,\r"
"Came whiffling through the tulgey wood,\r"
"And burbled as he came!\r"
"One, two!, One, two! And through and through\r"
"The vorpal blade went snicker-snack!\r"
"He left it dead, and with its head\r"
"He went galumphing back.\r"
"And hast thou slain the Jabberwock!\r"
"Come to my arms, my beamish boy!\r"
"Oh frabjous day! Callooh! Callay!\r"
"He chortled in his joy.\r"
"'Twas brillig, and the slithy toves\r"
"Did gyre and gimble in the wabe;\r"
"All mimsy were the borogoves,\r"
"And the mome raths outgabe.\r"
"-- Lewis Carroll (1832-98)";
ShowMessage(S);
}
Listing 13.7. The AboutDlg is a no-brainer. You dont
need to add any code to the default output generated by BCB.
///////////////////////////////////////
// File: AboutBox1.cpp
// Project: Address2
// Copyright (c) 1997 by Charlie Calvert
#include <vcl.h>
#pragma hdrstop
#include "AboutBox1.h"
#pragma resource "*.dfm"
TAboutBox *AboutBox;
__fastcall TAboutBox::TAboutBox(TComponent* AOwner)
: TForm(AOwner)
{
}
The special forms included in Listings 13.1 through 13.7 are the
FilterDlg and AboutBox. My feeling is that you can readily grasp
the concepts of these forms from just looking at the screen shots of them, shown
in Figures 13.4, 13.5, and 13.6.
FIGURE 13.4.
The FilterDlg from the Address2 program.
FIGURE 13.5.
The help screen from the Category dialog.
FIGURE 13.6.
The About box from the Address2 program.
The complete sample program, including all the forms shown here, is included
on the CD that accompanies this book. You will probably find it helpful to load
that program into BCB and refer to it from time to time while reading the
various technical discussions in the last half of this chapter.
Moving In and Out of Read-Only Mode
Perhaps the most important single function of the Address2 program is its
capability to move in and out of read-only mode. This capability is valuable
because it enables the user to open the program and browse through data without
ever having to worry about accidentally altering a record. In fact, when the
user first opens the program, typing into any of the data-aware controls should
be impossible. The only way for the program to get into edit mode is for the
user to click the Goto Edit button, which then automatically makes the data
live.
When the program is in read-only mode, the Insert and Cancel buttons are
grayed, and the Delete button is also dimmed. When the user switches into edit
mode, all these controls become live, and the text in the Goto Edit button is
changed so that it reads "Stop Edit". In other words, the caption for
the Edit button says either Goto Edit or Stop Edit, depending
on whether you are in read-only mode. I also use red and green colored bitmaps
to help emphasize the current mode and its capabilities. All these visual clues
help make the current mode of the program obvious to the user.
The functionality described is quite simple to implement. The key methods to
trace are the HandleEditMode and SetReadOnly methods.
The HandleEditMode routine is called from the CommandClick
method described in the preceding section:
void TForm1::HandleEditMode()
{
InsertBtn->Enabled = !DMod->AddressSource->AutoEdit;
CancelBtn->Enabled = !DMod->AddressSource->AutoEdit;
DeleteBtn->Enabled = !DMod->AddressSource->AutoEdit;
if (!DMod->AddressSource->AutoEdit)
{
SetReadOnly(True);
EditBtn->Caption = "Stop Edit";
Caption = FCaptionString + EDIT_MODE_STRING;
}
else
{
if (DMod->AddressTable->State != dsBrowse)
DMod->AddressTable->Post();
SetReadOnly(False);
EditBtn->Caption = "Goto Edit";
Caption = FCaptionString + READ_ONLY_STRING;
}
}
The primary purpose of this code is to ensure that the proper components are
enabled or disabled, depending on the current state of the program. After you
alter the appearance of the program, the code calls SetReadOnly:
void TForm1::SetReadOnly(BOOL NewState)
{
DMod->AddressSource->AutoEdit = NewState;
}
The center around which this routine revolves is the
AddressSource->AutoEdit property. When this property is set to False,
all the data-aware controls on the form are disabled, as shown in Figure 13.7,
and the user cannot type in them. When the property is set to True, the
data becomes live, as shown in Figure 13.8, and the user can edit or insert
records.
FIGURE 13.7.
Address2 as it appears in read-only mode.
FIGURE 13.8.
The Address2 program as it appears in edit mode.
The purpose of the AutoEdit property is to determine whether a
keystroke from the user can put a table directly into edit mode. When
AutoEdit is set to False, the user can't type information into a
data-aware control. When AutoEdit is set to True, the user can
switch the table into edit mode simply by typing a letter in a control. Note
that even when AutoEdit is set to False, you can set a table
into edit mode by calling AddressTable->Edit or
AddressTable->Insert. As a result, the technique shown here won't work
unless you gray out the controls that give the user the power to set the table
into edit mode. You should also be sure to set the dgEditing element of
the TDBGrids option property to False so that the user can
never type anything in this control. The grid is simply not meant for allowing
the user to modify records.
The code in the HandleEditMode method is concerned entirely with
interface issues. For instance, it enables or disables the Insert, Cancel, and
Delete controls, depending on whether the table is about to go in or out of
read-only mode. The code also ensures that the caption for the Edit button
provides the user with a clue about the button's current function. In other
words, the button doesn't report on the state of the program, but on the
functionality associated with the button.
The HandleEditMode method is written so that the program is always
moved into the opposite of its current state. At start-up time, the table should
be set to read-only mode (AutoEdit = False), and the appropriate
controls should be disabled. Thereafter, every time you click the Edit button,
the program will switch from its current state to the opposite state, from
read-only mode to edit mode, and then back again.
- ¡¡
¡¡
NOTE: In addition to the
TDataSource AutoEdit property, you can also take a table in and out of
read-only mode in a second way. This second method is really more powerful
than the first because it makes the table itself completely resistant to
change. However, this second method is more costly in terms of time and system
resources. The trick, naturally enough, is to change the ReadOnly
property of a TTable component.
You cannot set a table in or out of read-only mode while it is open.
Therefore, you have to close the table every time you change the ReadOnly
property. Unfortunately, every time you close and open a table, you are moved
back to the first record. As a result, you need to set a bookmark identifying
your current location in the table, close the table, and then move the table
in or out of read-only mode. When you are done, you can open the table and jet
back to the bookmark. This process sounds like quite a bit of activity, but in
fact it can usually be accomplished without the user being aware that anything
untoward has occurred.
With the Address2 program, clearly the first technique for moving a program in
and out of read-only mode is best. In other words, switching DataSource1
in and out of AutoEdit mode is much faster and much easier than
switching AddressTable in and out of read-only mode.
On the whole, the act of moving Address2 in and out of read-only mode is
fairly trivial. The key point to grasp is the power of the TDataSource
AutoEdit method. If you understand how it works, you can provide this same
functionality in all your programs.
Sorting Data
At various times, you might want the records stored in the program to be
sorted by first name, last name, or company. These three possible options are
encapsulated in the program's menu, as depicted in Figure 13.9, and also in an
enumerated type declared in Main.h:
enum TSearchSortType {stFirst, stLast, stCompany};
FIGURE 13.9.
The Sorts menu has three different options.
Once again, the Tag field from the Sorts drop-down menu makes it
possible to detect which option the user wants to select:
void TForm1::DoSort(TObject *Sender)
{
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case stFirst:
DMod->AddressTable->IndexName = "FNameIndex";
break;
case stLast:
DMod->AddressTable->IndexName = "LNameIndex";
break;
case stCompany:
DMod->AddressTable->IndexName = "CompanyIndex";
break;
}
}
If the user selects the menu option for sorting on the first name, then the
first element in the switch statement is selected; if the user opts to
sort on the last name, then the second element is selected, and so on.
Here is another way to state the same process: If the Tag property
for a menu item is zero, it is translated into stFirst; if the property
is one, it goes to stLast, and two goes to stCompany.
Everything depends on the order in which the elements of the enumerated type are
declared in the declaration for TSearchSortType. Of course, you must
associate a different value between 0 and 2 for the Tag property of
each menu item and then associate the following method with the OnClick
event for each menu item:
void __fastcall TForm1::CommandSortClick(TObject *Sender)
{
DoSort(Sender);
DMod->AddressTable->FindNearest(OPENARRAY(TVarRec, ("A")));
}
The CommandSortClick method receives input from the menus. After
finding out whether the user wants to sort by first name, last name, or company,
CommandSortClick asks DoSort to straighten out the indexes.
After sorting, a group of blank records might appear at the beginning of the
table. For example, if you choose to sort by the Company field, many of
the records in the Address table are not likely to have anything in the
Company field. As a result, several hundred, or even several thousand,
records at the beginning of the table might be of no interest to someone who
wants to view only companies. The solution, of course, is to search for the
first record that has a non-blank value in the Company field. You can
do so by using FindNearest to search for the record that has a
Company field that is nearest to matching the string "A". The
actual details of searching for a record are covered in the next section. The
downside of this process is that the cursor moves off the currently selected
record whenever you sort.
That's all there is to sorting the records in the Address2 program. Clearly,
this subject is not difficult. The key points to grasp are that you must create
secondary indexes for all the fields on which you want to sort, and then
performing the sort becomes as simple as swapping indexes.
Searching for Data
Searching for data in a table is a straightforward process. If you want to
search on the Company field, simply declaring a secondary index called
CompanyIndex is not enough. To perform an actual search, you must make
the CompanyIndex the active index and then perform the search. As a
result, before you can make a search, you must do three things:
- 1. Ask the user for the string he or she wants to find.
2. Ask the user for the field where the string resides.
3. Set the index to the proper field.
Only after jumping through each of these hoops are you free to perform the
actual search.
- ¡¡
¡¡
NOTE: Note that some databases
don't force you to search only on actively keyed fields. Some SQL servers, for
example, don't have this limitation. But local Paradox and dBASE tables are
restricted in this manner, so you must use the techniques described here when
searching for fields in these databases. If you chafe against these
limitations, you can use the OnFilterRecord process, in conjunction
with FindFirst, FindNext, and so on. The OnFilterRecord
process plays a role later in this program when you need to filter on the
marked field, which is of type Boolean and therefore cannot be
indexed.
I use the same basic algorithm in the Search portion of the program as I do
in the infrastructure for the sort procedure. The search method itself is simple
enough:
void __fastcall TForm1::CommandSearchClick(TObject *Sender)
{
AnsiString S;
if (InputQuery("Search Dialog", "Enter Name", S))
{
DoSort(Sender);
DMod->AddressTable->FindNearest(OPENARRAY(TVarRec, (S)));
}
}
This code retrieves the relevant string to search on from a simple
InputQuery dialog. InputQuery is a built-in VCL function that pops
up a dialog with an edit field in it. The first field of the call to
InputQuery defines the title of the dialog, the second contains the prompt
string, and the third contains the string you want the user to edit.
After you get the search string from the user, the DoSort method is
called to set up the indexes, and then the search is performed using
FindNearest. The assumption, of course, is that the menu items appear in
the same order as those for the sort process, and they have the same tags
associated with them.
Once again, the actual code for searching for data is fairly straightforward.
The key to making this process as simple as possible is setting up the
DoSort routine so that it can be used by both the Sorting and Searching
portions of the program.
Filtering Data
The Address2 program performs two different filtering chores. The first
involves allowing the user to see the set of records that fits in a particular
category. For example, if you have set the Category field in 20 records
to the string "Friend", then you can reasonably expect to be able to
filter out all other records that do not contain the word "Friend" in
the Category field. After you have this process in place, you can ask
the database to show you all the records that contain information about
computers or work or local business, and so on.
- ¡¡
¡¡
NOTE: In Chapter 15, "Working
with the Local InterBase Server," I show you how to set up many-to-many
relationships. They allow you to associate multiple traits with a single
record. For example, you can mark a record as containing the address of both a
"Friend" and a "Work" --related person. You can adopt the
techniques shown in that chapter to work with Paradox tables.
The second technique for filtering in the Address2 program involves the
Marked field. You might, for example, first use the Filter Category
technique to show only the records of your friends. Let's pretend you're
popular, so this list contains 50 people. You can then use the Marked
field to single out 10 of these records as containing the names of people you
want to invite to a party. After marking the appropriate records, you can then
filter on them so that the database now contains only the lists of your friends
who have been "marked" as invited to the party. In the chapter on printing, you
will see how to print this list on a set of labels. (If the "Friends" example is
too unbearably warm and cozy for you, you can think instead of sorting on the
list of clients who use a particular product and then marking only those you
want to receive a special mailing.)
In the next few paragraphs, I will tackle the Category filter first
and then explain how to filter on the Boolean Marked field.
Setting up a filter and performing a search are similar tasks. The first step
is to find out the category the user wants to use as a filter. To do so, you can
pop up a dialog that displays the CATS table to the user. The user can
then select a category and click the OK button. You don't need to write any
custom code for this dialog. Everything can be taken care of by the visual
tools.
Here is how to handle the process back in the main form:
void __fastcall TForm1::Filter1Click(TObject *Sender)
{
AnsiString S;
if (Filter1->Caption == "Filter")
{
if (FilterDlg->ShowModal() == mrOk)
{
S = DMod->CatsTableCATEGORY->Value;
if (S.Length() == 0)
return;
Filter1->Caption = "Cancel Filter";
DMod->AddressTable->IndexName = "CategoryIndex";
DMod->AddressTable->SetRange(OPENARRAY(TVarRec, (S)), OPENARRAY(TVarRec, (S)));
}
}
else
{
Filter1->Caption = "Filter";
DMod->AddressTable->CancelRange();
}
This code changes the Caption of the menu item
associated with filtering the Category field. If the Address table is
not currently filtered, then the menu reads "Filter". If the table is
filtered, then the menu reads "Cancel Filter". Therefore, the preceding
code has two sections: one for starting the filter and the second for canceling
the filter. The second part is too simple to merit further discussion, as you
can see from a glance at the last two lines of written code in the method.
¡¡
After allowing the user to select a category on which to search, the Address2
program sets up the CategoryIndex and then performs a normal filter
operation:
DMod->AddressTable->IndexName = "CategoryIndex";
DMod->AddressTable->SetRange(OPENARRAY(TVarRec, (S)), OPENARRAY(TVarRec, (S)));
This simple process lets you narrow the number of records displayed at any
one time. The key point to remember is that this whole process works only
because the user enters data in the Category field by selecting strings
from a drop-down combo box. Without the TDBComboBox, the number of
options in the Category field would likely become unmanageable.
Marking Files
The Marked field in this table is declared to be of type Boolean.
(Remember that one of the fields of ADDRESS.DB is actually called
Marked. In the first sentence, therefore, I'm not referring to an attribute
of a field, but to its name.)
On the main form for the program is a TSpeedButton component that
shows a switched-on light bulb if a field is marked and a switched-off light
bulb if a field is not marked. When the user scrolls up and down through the
dataset, the light bulb switches on and off depending on whether a field is
marked.
Here is a method for showing the user whether the Boolean Marked
field is set to True or False:
void __fastcall TForm1::AddressSourceDataChange(TObject *Sender,
TField *Field)
{
HBITMAP BulbOn, BulbOff;
if (DMod->AddressTable->FieldByName("Marked")->AsBoolean)
{
BulbOn = LoadBitmap((HINSTANCE)HInstance, "BulbOn");
SpeedButton1->Glyph->Handle = BulbOn;
}
else
{
BulbOff = LoadBitmap((HINSTANCE)HInstance, "BulbOff");
SpeedButton1->Glyph->Handle = BulbOff;
}
}
If the field is marked, a bitmap called BulbOn is loaded from one of
the program's two resource files. This bitmap is then assigned to the Glyph
field of a TSpeedButton. If the Marked field is set to
False, a second bitmap is loaded and shown in the TSpeedButton.
The bitmaps give a visual signal to the user as to whether the record is marked.
As I hinted in the preceding paragraph, the Address2 program has two resource
files. The first is the standard resource file, which holds the program's icon.
The second is a custom resource build from the following RC file:
BulbOn BITMAP "BULBON.BMP"
BulbOff BITMAP "BULBOFF.BMP"
This file is called BITS.RC, and it compiles to BITS.RES.
You should add BITS.RC to your project to make sure that the file is
compiled automatically and that it is linked into your program. Delphi
programmers take note: The automating of this process is a feature added to BCB
that is not present in Delphi!
Here are the changes the IDE makes to the Project Source file for your
application:
//--------------------------------------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
//--------------------------------------------------------------------------
USEFORM("Main.cpp", Form1);
USEDATAMODULE("DMod1.cpp", DMod);
USERES("Address2.res");
USEFORM("AboutBox1.cpp", AboutBox);
USEFORM("FileDlg1.cpp", FilterDlg);
USEFORM("Category1.cpp", CategoryDlg);
USERC("glyphs.rc");
The relevant line here is the last one. Note that you can get to the Project
Source by choosing View | Project Source from the BCB menu.
The AddressSourceDataChanged method shown previously in this section
is a delegated event handler for the AddressSource component in the
data module. The interesting point here, of course, is that AddressSource
is located in Form1, not in DMod1.
To associate a method from Form1 with an event located in DMod1,
you can write the following code in response to the OnShow event for
Form1:
void __fastcall TForm1::FormShow(TObject *Sender)
{
DMod->AddressSource->OnStateChange = AddressSourceStateChange;
AddressSourceStateChange(NULL);
DMod->AddressSource->OnDataChange = AddressSourceDataChange;
AddressSourceDataChange(NULL, NULL);
}
As you can see, I set up two event handlers, one for the OnStateChange
event and the other for the OnDataChange event. Now these methods will
be called automatically when changes occur in the data module.
- ¡¡
¡¡
NOTE: I suppose you could argue
whether the code from the FormShow event handler is an example of
good or bad coding practices. On the one hand, it seems to tie TForm1
and TDMod together with rather unseemly intimacy, but on the other
hand, it does so by working with a published interface of TDMod. The
key points in favor of it being good code are that you can safely decouple
TDMod from TForm1 without impairing the integrity or virtue of
TDMod. Furthermore, TForm1 uses only the published interface
of TDMod and does not require carnal knowledge of the most intimate
parts of TDMod. (Perhaps if I push this metaphor just a little
further, I can be the first technical writer to get involved in a censorship
dispute. On the other hand, that's probably not such a worthy goal, so I'll
discreetly end the note here.)
Whenever the user toggles the TSpeedButton on which the BulbOn
and BulbOff bitmaps are displayed, then the logical Marked
field in the database is toggled:
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
DMod->AddressTable->Edit();
DMod->AddressTableMarked->AsBoolean = !DMod->AddressTableMarked->AsBoolean;
DMod->AddressTable->Post();
}
Note that this code never checks the value of the Marked field--it
just sets that value to the opposite of its current state.
The program allows the user to show only the records that are currently
marked. This filter can be applied on top of the Category filter or can
be applied on a dataset that it is not filtered at all:
void __fastcall TForm1::ShowOnlyMarked1Click(TObject *Sender)
{
ShowOnlyMarked1->Checked = !ShowOnlyMarked1->Checked;
DMod->AddressTable->Filtered = ShowOnlyMarked1->Checked;
}
The preceding code sets the AddressTable into Filtered
mode. The OnFilterRecordEvent for the table, which is in the DMod1
unit, looks like this:
void __fastcall TDMod::AddressTableFilterRecord(TDataSet *DataSet,
bool &Accept)
{
Accept = (AddressTableMarked->AsBoolean == True);
}
Only the records that have the Marked field set to be True
will pass through this filter. If the table is filtered, therefore, only those
records that are marked are visible to the user.
If you give the user the ability to mark records, then you also probably
should give him or her the ability to clear all the marks in the program, or to
mark all the records in the current dataset and then possibly unmark a few key
records. For example, you might want to send a notice to all your friends,
except those who live out of town. To do so, you can first mark the names of all
your friends and then unmark the names of those who live in distant places.
The data module for the Address2 program contains a query with the following
SQL statement:
Update Address
Set Marked = :NewValue
This statement does not have a where clause to specify which records
in the Marked field you want to toggle. As a result, the code will
change all the records in the database with one stroke. Note how much more
efficient this method is than iterating through all the records of a table with
a while (!Table1->Eof) loop.
To use this SQL statement, you can write the following code:
void TDMod::ChangeMarked(BOOL NewValue)
{
ChangeMarkedQuery->Close();
if (NewValue)
ChangeMarkedQuery->ParamByName("NewValue")->AsString = "T";
else
ChangeMarkedQuery->ParamByName("NewValue")->AsString = "F";
ChangeMarkedQuery->ExecSQL();
AddressTable->Refresh();
}
This method sets the "NewValue" field of the query to T or
F, depending on how the method is called.
The following self-explanatory method responds to a menu click and uses the
ChangeMarked method:
void __fastcall TForm1::ClearAllMarks1Click(TObject *Sender)
{
DMod->ChangeMarked(False);
}
That's all I'm going to say about the filters in this program. This subject,
like the searching and sorting topics, is extremely easy to master. One of the
points of this chapter is how easily you can harness the power of BCB to write
great code that produces small, easy-to-use, robust applications.
Setting Colors
Using the Colors menu, shown in Figure 13.10, you can set the colors for most
of the major objects in the program. The goal is not to give the user complete
control over every last detail in the program, but to let him or her customize
the most important features. Even if you're not interested in giving the user
the ability to customize colors in your application, you may still be interested
in this section because I discuss Run Time Type Information (RTTI), as well as a
method for iterating over all the components on a form.
FIGURE 13.10.
The options under the Colors menu enable you to change the appearance of the
Address2 program.
The ColorClick method uses the time-honored method of declaring an
enumerated type and then sets up the Tag property from a menu item to
specify the selection of a particular option. Here is the enumerated type in
question:
enum TColorType {ccForm, ccEdit, ccEditText, ccLabel, ccPanel};
The routine begins by enabling the user to select a color from the Colors
dialog and then assigns that color to the appropriate controls:
void __fastcall TForm1::CommandColorClick(TObject *Sender)
{
ColorDialog1->Color = GetColor(Sender);
if (!ColorDialog1->Execute())
return;
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case ccForm:
Form1->Color = ColorDialog1->Color;
break;
case ccEdit:
SetEdits(ColorDialog1->Color);
break;
case ccEditText:
SetEditText(ColorDialog1->Color);
break;
case ccLabel:
SetLabels(ColorDialog1->Color);
break;
case ccPanel:
SetPanels(ColorDialog1->Color);
break;
}
}
If the user wants to change the form's color, the code to do so is simple
enough:
Form1->Color = ColorDialog1->Color;
However, changing the color of all the data-aware controls is a more
complicated process. To accomplish this goal, the ColorClick method
calls the SetEdits routine:
void TForm1::SetEdits(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
{
if (dynamic_cast<TDBEdit *>(Components[i]))
dynamic_cast<TDBEdit *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBGrid *>(Components[i]))
dynamic_cast<TDBGrid *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBMemo *>(Components[i]))
dynamic_cast<TDBMemo *>(Components[i])->Color = Color;
else if (dynamic_cast<TDBLookupComboBox *>(Components[i]))
dynamic_cast<TDBLookupComboBox *>(Components[i])->Color = Color;
}
}
This code iterates though all the components belonging to the main form of
the program and checks to see if any of them are TDBEdits,
TDBComboBoxes, or TDBMemos. When it finds a hit, the code casts
the control as a TDBEdit and sets its color to the new value selected
by the user:
dynamic_cast<TDBEdit *>(Components[i])->Color = Color;
Because this code searches for TDBEdits, TDBMemos, and
TDBGrids, it will very quickly change all the data-aware controls on the
form to a new color. Note that you need to check whether the dynamic cast will
succeed before attempting to change the features of the controls. If you try to
do it all in one step, an access violation will occur.
The code for setting labels and panels works exactly the same way as the code
for the data-aware controls. The only difference is that you don't need to worry
about looking for multiple types of components:
void TForm1::SetLabels(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TLabel *>(Components[i]))
dynamic_cast<TLabel *>(Components[i])->Font->Color = Color;
}
void TForm1::SetPanels(TColor Color)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TPanel *>(Components[i]))
dynamic_cast<TPanel *>(Components[i])->Color = Color;
}
After you set up a set of routines like this, you can write a few custom
routines that quickly set all the colors in the program to certain predefined
values:
void __fastcall TForm1::System1Click(TObject *Sender)
{
SetEdits(clWindow);
SetEditText(clBlack);
SetLabels(clBlack);
SetPanels(clBtnFace);
Form1->Color = clBtnFace;
}
void __fastcall TForm1::Defaults1Click(TObject *Sender)
{
SetEdits(clNavy);
SetEditText(clYellow);
SetLabels(clBlack);
SetPanels(clBtnFace);
Form1->Color = clBtnFace;
}
void __fastcall TForm1::Blues1Click(TObject *Sender)
{
SetEdits(0x00FF8080);
SetEditText(clBlack);
SetLabels(clBlack);
SetPanels(0x00FF0080);
Form1->Color = clBlue;
}
The System1Click method sets all the colors to the default system
colors as defined by the current user. The Default1Click method sets
colors to values that I think most users will find appealing. The
Blues1Click method has a little fun by setting the colors of the form to
something a bit unusual. The important point here is that the methods shown in
this section of the chapter show how to perform global actions that affect all
the controls on a form.
Clearly, taking control over the colors of the components on a form is a
simple matter. The matter of saving the settings between runs of the program is
a bit more complicated. The following section, which deals with the Registry,
focuses on the matter of creating persistent data for the user-configurable
parts of a program.
Working with the Registry
The simplest way to work with the Registry is with the TRegIniFile
class that ships with BCB. This object is a descendent of TRegistry.
TRegistry is meant exclusively for use with the Windows Registry, but
TRegIniFile also works only with the Registry; however, it uses methods
similar to those used with an INI file. In other words, TRegIniFile is
designed to smooth the transition from INI files to the Registry and to make it
easy to switch back if you want.
I am not, however, interested in the capability of TRegIniFile to
provide compatibility with INI files. This book works only with the Registry. I
use TRegIniFile rather than TRegistry simply because the
former object works at a higher level of abstraction than the latter object.
Using TRegIniFile is easier than using TRegistry; therefore, I
like it more. I can get my work done faster with it, and I am less likely to
introduce a bug. The fact that knowing it well means that I also know how to
work with INI files is just an added bonus, not a deciding factor.
- ¡¡
¡¡
NOTE: Again, I find myself
wrestling against my tendency to use TRegistry because it is a parent
of TRegIniFile. It is therefore smaller and perhaps somewhat faster.
However, I have to remember that my primary goal is to actually finish
applications that are as bug free as possible. If I find that the completed
application is too slow, and I have time left in my schedule, then I can worry
about optimizations. The key is just to get things done.
Furthermore, entering code in the Registry is an extremely bad candidate for
speed or size optimizations. This just is not a major bottleneck in most
programs, and the difference in size between TRegIniFile and
TRegistry is too small to have a significant impact on my program.
Therefore, I go with the simplest possible solution, unless I have a good
reason for creating extra work for myself. The worst crime is spending hours,
days, or even weeks optimizing a part of a program that doesn't have much
impact on code size or program performance.
The Registry is a fairly complex topic, so I have created a separate program
called RegistryDemo that shows how to get around in it. Once you are
clear on the basics, I can come back to the Address2 program and add Registry
support.
- ¡¡
¡¡
NOTE: When you're working with
the Registry, damaging it is always possible. Of course, I don't think any of
the code I show you in this book is likely to damage the Registry; it's just
that being careful is always a good idea. Even if you're not writing code that
alters the Registry, and even if you're not a programmer, you should still
back up the Registry, just to be safe.
I've never found a way to recover a badly damaged Registry. Whenever my
Registry has been mangled by a program, I've always had to reinstall Windows
from scratch.
Each time Windows starts, it saves a previous copy of the Registry in the
Windows directory under the name SYSTEM.DA0. The current working
version of the Registry is in SYSTEM.DAT. The Registry is made up of
read-only, hidden system files, so you need to make sure you go to View |
Options in the Explorer and turn off that silly business about hiding files of
certain types.
You should probably back up your current copy of SYSTEM.DAT from time
to time, and you should always remember that if the worst happens,
SYSTEM.DA0 holds a good copy of the Registry for you, at least until the
next time you successfully restart Windows.
If you right-click a filename in the Explorer, you can pop up the Properties
dialog, which lets you change the attributes of the file, such as whether it
is hidden. More information is available from the Microsoft MSDN in the
document called "Backing Up the Registry or Other Critical Files."
The RegistryDemo application, found on the CD that accompanies this book,
exists only to show you how to work with the Registry. The code for this
application is shown in Listings 13.8 and 13.9.
Listing 13.8. The header file for the RegistryDemo
application.
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include <vcl\ExtCtrls.hpp>
class TForm1 : public TForm
{
__published: // IDE-managed Components
TListBox *ListBox1;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *OpenRegistry1;
TMenuItem *N1;
TMenuItem *MakeHomeinTheRegistry1;
TMenuItem *N2;
TMenuItem *Exit1;
TPanel *Panel1;
TListBox *ListBox2;
TMenuItem *BackOneLevel1;
TMenuItem *RegisterEXE1;
void __fastcall MakeHomeInRegistryClick(TObject *Sender);
void __fastcall OpenRegistry1Click(TObject *Sender);
void __fastcall Exit1Click(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall ListBox1DblClick(TObject *Sender);
void __fastcall BackOneLevel1Click(TObject *Sender);
void __fastcall ListBox1Click(TObject *Sender);
void __fastcall RegisterEXE1Click(TObject *Sender);
private: // User declarations
TRegIniFile *FViewReg;
public: // User declarations
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 13.9. The main module for the RegistryDemo
application.
///////////////////////////////////////
// File: Main.cpp
// Project: RegistryDemo
// Copyright (c) 1997 Charlie Calvert
#include <vcl\vcl.h>
#include <regstr.h>
#include <vcl\registry.hpp>
#pragma hdrstop
#include "Main.h"
#include "codebox.h"
//--------------------------------------------------------------------------
#pragma resource "*.dfm"
TForm1 *Form1;
//--------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FViewReg = new TRegIniFile("");
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
FViewReg->Free();
}
void __fastcall TForm1::Exit1Click(TObject *Sender)
{
Close();
}
void __fastcall TForm1::ViewRegistry1Click(TObject *Sender)
{
Panel1->Caption = FViewReg->CurrentPath;
if (FViewReg->HasSubKeys())
FViewReg->GetKeyNames(ListBox1->Items);
ListBox1Click(NULL);
}
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
int i = ListBox1->ItemIndex;
if (i < 0)
i = 0;
AnsiString S = ListBox1->Items->Strings[i];
TRegIniFile *RegFile = new TRegIniFile(FViewReg->CurrentPath + "\\" + S);
if (RegFile->HasSubKeys())
RegFile->GetKeyNames(ListBox2->Items);
else
RegFile->GetValueNames(ListBox2->Items);
RegFile->Free();
}
void __fastcall TForm1::ListBox1DblClick(TObject *Sender)
{
AnsiString S = ListBox1->Items->Strings[ListBox1->ItemIndex];
FViewReg->OpenKey(S, False);
ViewRegistry1Click(NULL);
}
void __fastcall TForm1::BackOneLevel1Click(TObject *Sender)
{
AnsiString S = FViewReg->CurrentPath;
AnsiString Temp;
Caption = S;
if (S.Length() != 0)
{
if (S[1] != `\\')
S = "\\" + S;
Temp = StripLastToken(S, `\\', Temp);
if (Temp.Length() == 0)
Temp = "\\";
FViewReg->OpenKey(Temp, False);
ViewRegistry1Click(NULL);
}
}
void __fastcall TForm1::MakeHomeInRegistryClick(TObject *Sender)
{
TRegIniFile *RegFile = new TRegIniFile("SOFTWARE\\Charlie's Stuff");
RegFile->WriteString("Colors", "Form", "1");
RegFile->WriteString("Colors", "Edit Text", "1");
RegFile->WriteString("Colors", "Panels", "1");
RegFile->WriteString("Colors", "Labels", "1");
RegFile->WriteString("Colors", "Edits", "1");
RegFile->Free();
}
void __fastcall TForm1::RegisterEXE1Click(TObject *Sender)
{
AnsiString Path(ParamStr(0));
Path = ExtractFilePath(Path);
TRegIniFile *Registry = new TRegIniFile("");
Registry->RootKey = HKEY_LOCAL_MACHINE;
Registry->OpenKey(REGSTR_PATH_APPPATHS, false);
Registry->WriteString("RegistryDemo.exe", "", Path + "registrydemo.exe");
Registry->WriteString("RegistryDemo.exe", "Path", Path);
// ShowMessage(REGSTR_PATH_APPPATHS);
Registry->Free();
}
This rather loosely put-together demo shows off several features of the
Registry, with only a minimum of error checking. In particular, the preceding
code shows how to save the settings for a program in the Registry, to register
an application in the Registry, and to iterate back and forth through one major
branch of the Registry.
The purpose of this program is simply to illustrate in one place most of the
key tasks you can perform with the Registry. Parts of the program give you some
of the features of the Windows utility called RegEdit.exe, but it does
not attempt to duplicate this technology because RegEdit works well enough on
its own. However, if your heart is set on creating an advanced editor for the
Registry, the RegistryDemo program would at least get some of the basic grunt
work out of the way for you.
Here's the simplest thing you can do with the TRegIniFile object:
TRegIniFile *RegFile = new TRegIniFile("SammysEntry");
RegFile->Free();
These two lines of code add a key called "SammysEntry" to the
Registry. If you've ever tried the tiresome task of manipulating the Registry
using raw Windows API calls, then these two simple lines of code may suggest to
you how much time the TRegIniFile can save programmers.
By default, TRegIniFile starts working in HKEY_CURRENT_USER.
Therefore, after running the two lines shown here, you can go to the Run menu,
type REGEDIT, click OK, and launch the Windows program that lets you
explore the Registry. If you open the HKEY_CURRENT_USER branch of the
program, you will see the Registry entry shown in Figure 13.11.
FIGURE 13.11.
The Registry after passing the string "SammysEntry" to the
TRegIniFile constructor.
If you're not concerned about OLE and you are working with the Registry, you
care about two major keys:
- HKEY_LOCAL_MACHINE\Software: Here you register your application
with the system. Typical programs place only a few general pieces of
information in this key.
¡¡
- HKEY_CURRENT_USER\Software: Here you define the current settings
for your program. This part of the Registry replaces the old INI files used in
Windows 3.1. For example, if you want to save the location of a window or the
size of a font, then you save them here. Many entries in this section are
extremely detailed and prolix. For example, check out the entries beneath
Borland C++Builder in the Registry. You will find a long, unfriendly list of
the many changes you can make to the IDE via the menus and dialogs of BCB.
After you consider the information laid out in the preceding bullet points,
you should clearly see you would rarely want to store any information directly
off HKEY_CURRENT_USER. A more likely place to store information would
be off the Software key beneath HKEY_CURRENT_USER.
- ¡¡
¡¡
NOTE: If you've never worked
with the Registry before, it can appear a bit daunting at first. Indeed, I am
not convinced that it was the simplest way to solve the problems it handles.
However, if you spend time with the Registry, little by little you will
unravel its secrets. Certainly, it's much more complex than the old
Autoexec.bat and Config.sys files that Intel-based programmers
have wrestled with for years. However, just as we all slowly became familiar
with the intricacies of the DOS start-up files, so do we learn how to become
familiar with the Registry.
If you want to write into HKEY_CURRENT_USER\Software rather than
into the root of HKEY_CURRENT_USER, you could write the following lines
of code:
TRegIniFile *RegFile = new TRegIniFile("SOFTWARE\\Charlie's Stuff");
RegFile->Free();
These lines create a new key in the Registry, as shown in Figure 13.12. If
you want to change the base key from which you start writing code, then you can
write the following:
Registry->RootKey = HKEY_LOCAL_MACHINE;
Now the code you write will go under HKEY_LOCAL_MACHINE rather than
under HKEY_CURRENT_USER.
The settings shown in Figure 13.12 are probably closer to what most
programmers want to achieve than the results of the first effort. Now your code
is listed right up there next to Borland's, Microsoft's, Netscape's, and all the
others who will futilely attempt to compete with you for market share.
- ¡¡
¡¡
NOTE: If I were conducting a
study exploring which companies have the largest shares of the Windows market,
a cross section of the HKEY_CURRENT_USER\Software portion of the
Registry from a large number of machines might provide some valuable clues!
FIGURE 13.12.
Setting up a home for your program's settings under HKEY_CURRENT_USER
\Software.
Now that you have set up everything properly, you can start storing
information in the Registry. In particular, our current goal is to save the
colors for the key features of the Address2 program after the user has set them
at runtime. Back in the old days, when INI files were in fashion, you might have
created an INI file with this information in it:
[Colors]
Form=8421440
Edits=8421376
EditText=0
Labels=0
Panels=12639424
This cryptic information might be stored in a text file called
Address2.ini and read at runtime by using calls such as
ReadPrivateProfileString or by using the TIniFile object that
ships with BCB. This INI file has a single section in it called Colors,
and under the Colors section are five entries specifying the colors for
each of the major elements in the program. Translated into the language of the
Registry, this same information looks like the Registry Editor window captured
in Figure 13.13.
Here is how to use the TRegIniFile class to write code that enters
the pertinent information into the Registry:
TRegIniFile *RegFile = new TRegIniFile("SOFTWARE\\Charlie's Stuff");
RegFile->WriteString("Colors", "Form", "1");
RegFile->WriteString("Colors", "Edit Text", "1");
RegFile->WriteString("Colors", "Panels", "1");
RegFile->WriteString("Colors", "Labels", "1");
RegFile->WriteString("Colors", "Edits", "1");
RegFile->Free();
|