Overview
Now you can take a look at a real relational database in action. The
preceding six chapters have really been nothing but a long prelude to this
chapter, where all the pieces finally come together.
This chapter features another address book program, but this time it will be
based on a relational database. This second database will allow you to add
multiple addresses, phone numbers, and e-mail addresses to each name in the
address book.
Subjects covered in this chapter include the following:
- Creating the tables for a relational database
- Cascading deletes
- At runtime, iterating through the controls on a form or a data module and
performing certain actions on selected components. For instance, the code
shows how to iterate through all the tables on TDataModule to make
sure that they are all posted.
- Working with the TPageControl and TTabSheet objects
- Working with TTreeView, TImageList, and TTreeNode
- Retrieving error strings from a resource
- Using the TSession object
After you get a look at the address book program, I start a second program
called kdAddExplore. This program looks and feels a lot like a miniature version
of the Database Explorer. You can use this program to explore the structure of
the five tables used in the address book program found in the first half of this
chapter.
The main purpose of the kdAddExplore program is to let you see some of the
functionality of the global TSession object that is automatically
available in all BCB database applications. The Session object is
created automatically when you call Application->Initialize at the
startup of a database program. This call is found in the program source
generated for you by BCB. To view the program source, choose View | Program
Source from the main BCB menu. Don't forget to check the readme file on the CD
that comes with this book for additional information about the aliases used in
this book.
Data in the Real World
The code in this chapter addresses the kinds of problems you find in
real-world situations. In particular, the conflict that needs to be resolved is
between the rigid, inflexible nature of simple tables and the fluid,
kaleidoscope-like nature of information in the real world.
When most people first try to build database programs, they tend to create
one simple table, like the one shown in the Address2 program from the preceding
chapter. The limitations with that kind of system might well emerge on the first
day of use. For example, you might start transferring handwritten addresses into
the database. At first, this process might go fairly well, but then you are
likely to encounter a situation in which one person has multiple phone numbers
or multiple addresses. It's not at all unusual for one person to have three or
more e-mail addresses. The Address2 program does not have a good solution for
that kind of problem.
In professional settings, this problem can be multiplied many times. For
example, I need to track all the Borland offices in the world. This task
involves tracking addresses in Germany, England, France, Australia, Hong Kong,
Japan, and various other locations throughout the world.
My job puts me in contact with a number of software vendors (ISVs) that use
or create Borland tools. Many of these people maintain offices both at home and
at their businesses. Some of them frequent certain sites, and others have
complex relationships with their companies that I can track only via freehand
notes.
As you can see, information in the real world is messy and complex. The
Address2 program is simple and straightforward. Many people can make do with
simple tools, but others need to have a more sophisticated system.
The kdAdd program found in this chapter is an attempt to resolve the kinds of
problems you find in real-world situations. In the form you see it here, it is
not quite as polished as the Address2 program. It is, however, much more
sophisticated and much more powerful. With some work, it could easily form the
basis for a professional-level database used in an office.
Examining the Relational Address Program
The kdAdd program uses five tables called kdNames, kdAddress,
kdPhone, kdMemo, and kdEmail. The kdNames
table is the master table that "owns" the other four tables. The other tables
are detail tables.
When the program first appears, it looks like the image shown in Figure 14.1.
As you can see, the program uses a TPageControl with five pages, one
for each of the tables. The Address and Phone tables are also shown on the first
page so that the user can see them easily. If you want to delete items from
either an
address or phone number, then you should turn to the respective pages for
those items.
The data for the program is kept in the Data directory on the CD
that comes with this book. You should create a Standard Paradox alias called
CUNLEASHED that points to a copy of this directory, which resides on your
hard drive. Refer to the readme file on the CD that comes with this book if you
need additional information setting up the alias. The fields for the tables in
the database are shown in Table 14.1 through Table 14.5.
FIGURE 14.1. The main screen for the kdAdd
program.
Table 14.1. The format for the kdName table.
| Table name |
Type |
Size |
Primary index |
| NameCode |
+ |
|
* |
| FirstName |
A |
30 |
|
| LastName |
A |
30 |
|
| Company |
A |
30 |
|
Table 14.2. The structure for the kdAdd table.
| Table name |
Type |
Size |
Primary index |
| AddCode |
+ |
|
* |
| Address1 |
A |
30 |
|
| Address2 |
A |
30 |
|
| City |
A |
30 |
|
| State |
A |
3 |
|
| Zip |
A |
10 |
|
| NameCode |
I |
|
|
Table 14.3. The structure for the kdPhone table.
| Table name |
Type |
Size |
Primary index |
| PhoneCode |
+ |
|
* |
| Description |
A |
15 |
|
| Number |
A |
25 |
|
| Ext |
A |
5 |
|
| NameCode |
I |
|
|
Table 14.4. The structure for the kdEMail table.
| Table name |
Type |
Size |
Primary index |
| EMailCode |
+ |
|
* |
| Address |
A |
50 |
|
| Description |
A |
65 |
|
| Service |
A |
25 |
|
| NameCode |
I |
|
|
Table 14.5. The structure for the kdMemo table.
| Table name |
Type |
Size |
Primary index |
| MemoCode |
+ |
|
* |
| Description |
A |
25 |
|
| MemoData |
M |
15 |
|
| NameCode |
I |
|
|
Four constraints are placed on the table in the form of foreign keys called
NameCode. They are placed in each of the program's tables except for the
master table. These constraints are shown in Figure 14.2.
When you're viewing this information in the Database Explorer (DBX), you
should highlight the name of each constraint and then look at the definition
page to read the Reference Type and Reference To fields. As
you can see from Figure 14.2, these fields show which table the constraint
references. The view shown here is of the kdNames table, and it is the
master table in these relationships. The riAddNameCode constraint
references the kdAdds table.
FIGURE 14.2. By opening the Referential
Integrity section of kdNames in DBX, you can see the foreign keys used
by the database.
Table 14.6 shows another way to think about the referential integrity in this
database.
Table 14.6. The keys in the database shown in table format.
| Table |
Primary key |
Foreign key |
| kdName |
NameCode |
|
| kdAdd |
AddCode |
NameCode references kdName.NameCode |
| kdPhone |
PhoneCode |
NameCode references kdName.NameCode |
| kdEMail |
EMailCode |
NameCode references kdName.NameCode |
| kdMemo |
MemoCode |
NameCode references kdName.NameCode |
As you can see, kdAdd, kdPhone, kdEmail, and
kdMemo all have a single foreign key called NameCode that
references the NameCode primary key in kdNames. The number in
the foreign keys therefore must be a number found in the primary key of the
kdNames table. Furthermore, you cannot delete a row from the kdNames
table unless all its related fields in the other tables have been deleted first.
You also cannot change the value in the NameCode field of kdNames
if it will leave records in the other tables "stranded."
-
NOTE: If you are new to
referential integrity, take the time to play with the database and test these
constraints. Referential integrity exists to prevent the user from
accidentally deleting needed data, and from accidentally entering erroneous
data. During the course of this chapter, you should test the restraints on
this database so you can see how it establishes rules that help both the
programmer and the user maintain a valid set of data.
The referential integrity relationships you see here represent the classic
simplest case for constructing a real relational database. Most databases in the
real world have more tables and more foreign keys. However, this one has all the
elements of a real relational database, and the complexity level is sufficient
for programmers who are new to this kind of programming.
Several secondary indexes are used in this program, as shown in Table 14.7
through Table 14.11. Most of these indexes are the result of the foreign keys on
the NameCode fields of kdAdds, kdPhone, kdEMail,
and kdMemo. However, indexes are also set up on the Company,
FirstName, and LastName fields of the kdNames table.
Note that idxLastName consists of both the last name and first name of
each entry in the kdNames table. This convention is helpful when
sorting lists wherein you have more than one entry with a particular last name.
For instance, if you have two people with the last name of Jones, creating a key
on the last and first names will ensure that Able Jones is listed before Betty
Jones. If you further study the tables shown here, you will see that the
kdPhone and kdMemo tables also have indexes on their description
fields.
Table 14.7. The indexes on the kdNames table.
| Table |
Index fields |
| idxCompany |
Company |
| idxFirstName |
FirstName |
| idxLastName |
LastName, FirstName |
Table 14.8. The index on the kdAdds table.
| Table |
Index fields |
| NameCode |
NameCode |
Table 14.9. The indexes on the kdPhone table.
| Index |
Index fields |
| idxDescription |
Description |
| NameCode |
NameCode |
Table 14.10. The index on the kdEmail table.
| Index |
Index fields |
| NameCode |
NameCode |
Table 14.11. The indexes on the kdMemo table.
| Index |
Index fields |
| NameCode |
NameCode |
| idxDescription |
Description |
You now know all the core facts about the kdAdds program. After you have laid
out the tables as shown here, all the really heavy work in constructing the
program is completed. You still have considerable work to do in creating a front
end for the program, but the core work for the project is done after you have
created the tables and defined the ways in which they interrelate.
The Code for kdAdds
The code for the kdAdds program is shown in Listings 14.1 through 14.8.
Notice the custom RC file and the include file for the project. These
two tiny files store error strings. In later versions of the program, these
files will become larger as more error strings are added to the code. For this
release, all I have done is stub out these files so that they can easily be
expanded later.
Listing 14.1. The header for the main module in the
kdAdds program.
///////////////////////////////////////
// File: Main.h
// Project: kdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <Forms.hpp>
#include <StdCtrls.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <DBCtrls.hpp>
#include <Mask.hpp>
#include <DBGrids.hpp>
#include <Grids.hpp>
#include <Menus.hpp>
#include <ExtCtrls.hpp>
#include <ComCtrls.hpp>
#include <DBCGrids.hpp>
#include <vcl\Buttons.hpp>
class TForm1 : public TForm
{
__published:
TMainMenu *MainMenu1;
TMenuItem *Sorts1;
TMenuItem *Last1;
TMenuItem *First1;
TMenuItem *Company1;
TPanel *Panel1;
TDBGrid *FirstLastGrid;
TPageControl *PageControl1;
TTabSheet *tsAddress;
TTabSheet *tsPhone;
TTabSheet *tsMemo;
TDBGrid *DBGrid2;
TDBMemo *DBMemo1;
TDBCtrlGrid *DBCtrlGrid2;
TLabel *Label1;
TLabel *Label2;
TLabel *Label3;
TLabel *Label4;
TLabel *Label5;
TDBEdit *DBEdit1;
TDBEdit *DBEdit2;
TDBEdit *City;
TDBEdit *DBEdit4;
TDBEdit *Zip;
TTabSheet *tsEMail;
TDBCtrlGrid *DBCtrlGrid3;
TLabel *Label12;
TDBEdit *DBEdit10;
TLabel *Label13;
TDBEdit *DBEdit11;
TLabel *Label14;
TDBEdit *DBEdit12;
TMenuItem *Table1;
TMenuItem *Insert1;
TMenuItem *Delete1;
TMenuItem *Post1;
TSpeedButton *InsertBtn;
TSpeedButton *PostBtn;
TSpeedButton *DeleteBtn;
TSpeedButton *CancelBtn;
TMenuItem *Cancel1;
TMenuItem *Seach1;
TMenuItem *First2;
TMenuItem *Last2;
TMenuItem *Company2;
TMenuItem *Help1;
TMenuItem *Contents1;
TMenuItem *N1;
TMenuItem *About1;
TTabSheet *AddInfo1;
TLabel *Label15;
TLabel *Label16;
TLabel *Label17;
TDBEdit *DBEdit13;
TDBEdit *DBEdit14;
TDBEdit *DBEdit15;
TDBCtrlGrid *DBCtrlGrid4;
TLabel *Label18;
TLabel *Label19;
TLabel *Label20;
TLabel *Label21;
TLabel *Label22;
TDBEdit *DBEdit17;
TDBEdit *DBEdit18;
TDBEdit *DBEdit19;
TDBEdit *DBEdit20;
TDBCtrlGrid *DBCtrlGrid5;
TLabel *Label23;
TLabel *Label24;
TLabel *Label25;
TDBEdit *DBEdit21;
TDBEdit *DBEdit22;
TDBEdit *DBEdit23;
TDBEdit *DBEdit6;
TDBCtrlGrid *DBCtrlGrid1;
TLabel *Label6;
TLabel *Label7;
TLabel *Label8;
TDBEdit *DBEdit7;
TDBEdit *DBEdit8;
TDBEdit *DBEdit16;
void __fastcall Last1Click(TObject *Sender);
void __fastcall Insert1Click(TObject *Sender);
void __fastcall Cancel1Click(TObject *Sender);
void __fastcall Delete1Click(TObject *Sender);
void __fastcall Last2Click(TObject *Sender);
void __fastcall About1Click(TObject *Sender);
void __fastcall PostBtnClick(TObject *Sender);
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 14.2. The main module for the kdAdds
program.
///////////////////////////////////////
// File: Main.cpp
// Project: KdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "Main.h"
#include "DMod1.h"
#include "Globals.h"
#include "AboutBox1.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
PageControl1->ActivePage = AddInfo1;
}
void __fastcall TForm1::SetupIndex(TObject *Sender)
{
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case 100:
DMod->NamesTable->IndexName = "idxLastName";
break;
case 101:
DMod->NamesTable->IndexName = "idxFirstName";
break;
case 102:
DMod->NamesTable->IndexName = "idxCompany";
break;
case 103:
DMod->NamesTable->IndexName = "";
break;
}
}
void __fastcall TForm1::IndexClick(TObject *Sender)
{
SetupIndex(Sender);
DMod->NamesTable->FindNearest(OPENARRAY(TVarRec, ("AAAA")));
}
void __fastcall TForm1::Insert1Click(TObject *Sender)
{
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1:
DMod->NamesTable->Insert();
break;
case 2:
DMod->AddressTable->Insert();
break;
case 3:
DMod->PhoneTable->Insert();
break;
case 4:
DMod->EMailTable->Insert();
break;
}
}
void __fastcall TForm1::Cancel1Click(TObject *Sender)
{
AnsiString S;
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1:
DMod->NamesTable->Cancel();
break;
case 2:
DMod->AddressTable->Cancel();
break;
case 3:
DMod->PhoneTable->Cancel();
break;
case 4:
DMod->EMailTable->Cancel();
break;
default:
ShowMessage(GetError(1, S));
}
}
void __fastcall TForm1::Delete1Click(TObject *Sender)
{
AnsiString S;
AnsiString Msg("Are you sure you want to delete \r %s?");
Set<TMsgDlgBtn, 0, 8> Btns;
Btns << mbYes << mbNo;
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1:
Msg = Format(Msg, OPENARRAY(TVarRec,
(DMod->NamesTableFirstLastCompany->AsString)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->CascadingDelete();
break;
case 2:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->Address)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->AddressTable->Delete();
break;
case 3:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->Phone)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->PhoneTable->Delete();
break;
case 4:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->EMail)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->EMailTable->Delete();
break;
default:
ShowMessage(GetError(1, S));
}
}
void __fastcall TForm1::SearchClick(TObject *Sender)
{
AnsiString S, IndexName;
if (InputQuery("Search for Name", "Enter Name: ", S))
{
IndexName = DMod->NamesTable->IndexName;
SetupIndex(Sender);
DMod->NamesTable->FindNearest(OPENARRAY(TVarRec, (S)));
DMod->NamesTable->IndexName = IndexName;
}
}
void __fastcall TForm1::About1Click(TObject *Sender)
{
AboutBox->ShowModal();
}
void __fastcall TForm1::PostBtnClick(TObject *Sender)
{
DMod->PostAll();
}
Listing 14.3. The header for the data module.
///////////////////////////////////////
// File: DMod1.h
// Project: KdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef DMod1H
#define DMod1H
#include <Forms.hpp>
#include <DBTables.hpp>
#include <DB.hpp>
#include <Classes.hpp>
class TDMod : public TDataModule
{
__published:
TTable *NamesTable;
TDataSource *dsNames;
TTable *AddressTable;
TTable *PhoneTable;
TTable *MemoTable;
TDataSource *dsAddress;
TDataSource *dsPhone;
TDataSource *dsMemo;
TAutoIncField *PhoneTablePhoneCode;
TStringField *PhoneTableDescription;
TStringField *PhoneTableNumber;
TStringField *PhoneTableExt;
TIntegerField *PhoneTableNameCode;
TTable *EMailTable;
TDataSource *dsEmail;
TAutoIncField *EMailTableEMailCode;
TStringField *EMailTableAddress;
TStringField *EMailTableDescription;
TStringField *EMailTableService;
TIntegerField *EMailTableNameCode;
TQuery *EMailDeleteQuery;
TQuery *MemoDeleteQuery;
TQuery *PhoneDeleteQuery;
TQuery *AddressDeleteQuery;
TQuery *NamesDeleteQuery;
TAutoIncField *MemoTableMemoCode;
TStringField *MemoTableDescription;
TMemoField *MemoTableMemoData;
TIntegerField *MemoTableNameCode;
TAutoIncField *NamesTableNameCode;
TStringField *NamesTableFirstName;
TStringField *NamesTableLastName;
TStringField *NamesTableCompany;
TAutoIncField *AddressTableAddCode;
TStringField *AddressTableAddress1;
TStringField *AddressTableAddress2;
TStringField *AddressTableCity;
TStringField *AddressTableState;
TStringField *AddressTableZip;
TIntegerField *AddressTableNameCode;
TStringField *NamesTableFirstLastCompany;
void __fastcall NamesTableCalcFields(TDataSet *DataSet);
private:
AnsiString __fastcall GetAddress();
AnsiString __fastcall GetPhone();
AnsiString __fastcall GetEMail();
public:
virtual __fastcall TDMod(TComponent* Owner);
void PostAll(void);
void CascadingDelete(void);
__property AnsiString Address={read=GetAddress};
__property AnsiString Phone={read=GetPhone};
__property AnsiString EMail={read=GetEMail};
};
extern TDMod *DMod;
#endif
Listing 14.4. The code for the data module.
///////////////////////////////////////
// File: DMod1.cpp
// Project: KdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl.h>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
NamesTable->Open();
AddressTable->Open();
PhoneTable->Open();
MemoTable->Open();
EMailTable->Open();
}
void __fastcall TDMod::NamesTableCalcFields(TDataSet *DataSet)
{
AnsiString Temp = NamesTableFirstName->Value+ " " +NamesTableLastName->Value;
if (Temp == " ")
NamesTableFirstLastCompany->Value = NamesTableCompany->Value;
else
NamesTableFirstLastCompany->Value = Temp;
}
void DoPost(TDataSet *Data)
{
if ((Data->State == dsInsert) || (Data->State == dsEdit))
Data->Post();
}
void TDMod::PostAll(void)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TTable*>(Components[i]))
DoPost((TDataSet*)(Components[i]));
}
void TDMod::CascadingDelete(void)
{
EMailDeleteQuery->ParamByName("NameCode")->AsInteger =
EMailTableNameCode->Value;
EMailDeleteQuery->ExecSQL();
MemoDeleteQuery->ParamByName("NameCode")->AsInteger =
MemoTableNameCode->Value;
MemoDeleteQuery->ExecSQL();
PhoneDeleteQuery->ParamByName("NameCode")->AsInteger =
PhoneTableNameCode->Value;
PhoneDeleteQuery->ExecSQL();
AddressDeleteQuery->ParamByName("NameCode")->AsInteger =
AddressTableNameCode->Value;
AddressDeleteQuery->ExecSQL();
NamesDeleteQuery->ParamByName("NameCode")->AsInteger =
NamesTableNameCode->Value;
NamesDeleteQuery->ExecSQL();
NamesTable->Refresh();
}
AnsiString __fastcall TDMod::GetAddress()
{
return DMod->AddressTableAddress1->AsString + `\r' +
DMod->AddressTableAddress2->AsString + `\r' +
DMod->AddressTableCity->AsString + `\r' +
DMod->AddressTableState->AsString + `\r' +
DMod->AddressTableZip->AsString;
}
AnsiString __fastcall TDMod::GetPhone()
{
return DMod->PhoneTableDescription->AsString + `\r' +
DMod->PhoneTableNumber->AsString + `\r' +
DMod->PhoneTableExt->AsString;
}
AnsiString __fastcall TDMod::GetEMail()
{
return DMod->EMailTableAddress->AsString + `\r' +
DMod->EMailTableDescription->AsString + `\r' +
DMod->EMailTableService->AsString;
}
Listing 14.5. The header for the Globals unit.
///////////////////////////////////////
// File: Globals.h
// Project: KdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef GlobalsH
#define GlobalsH
AnsiString GetError(int ErrNo, AnsiString &S);
#endif
Listing 14.6. The main module for the Globals unit.
///////////////////////////////////////
// File: Globals.cpp
// Project: KdAdd
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Globals.h"
#define ERR_STRING_SIZE 255
AnsiString GetError(int ErrNo, AnsiString &S)
{
S.SetLength(ERR_STRING_SIZE);
LoadString(HINSTANCE(HInstance), 1, S.c_str(), ERR_STRING_SIZE);
return S;
}
Listing 14.7. The custom RC file for the project.
This is a stub to be filled out later.
#include "kderrs.inc"
STRINGTABLE
{
KDERR_CASESTATEMENT, "Command fell through case statement"
}
Listing 14.8. The include file for the project has
only one entry. This is a stub to be filled out later.
#define KDERR_CASESTATEMENT 1
The pages in the TPageControl are hidden from view in Figure 14.1.
In Figure 14.3 through Figure 14.6, you can see the remaining TTabSheet
objects.
FIGURE 14.3. The tab sheet for the Address
table.
FIGURE 14.4. The tab sheet for the Phone
table.
FIGURE 14.5. The tab sheet for the E-mail
table.
Using the kdAdd Program
The kdAdd program has the minimal functionality needed to support the user's
needs. For example, you can perform Insert, Post, Delete, and Cancel operations
on all the tables. Access to these features is provided through both the menus
and a speedbar. You can also set the index to the Company, First,
or Last fields of the kdNames table. Finally, you can search
on either the Company, First, or Last fields.
FIGURE 14.6. The tab sheet for Memo.
Setting Up the Index for kdAdd
One of the hubs around which the kdAdd program revolves involves the code
that controls the index for the program. This code is called from several
different places in the program. The obvious place to start studying it,
however, is in the response method for the menu items that let the user change
the index:
void __fastcall TForm1::SetupIndex(TObject *Sender)
{
switch (dynamic_cast<TComponent *>(Sender)->Tag)
{
case 100:
DMod->NamesTable->IndexName = "idxLastName";
break;
case 101:
DMod->NamesTable->IndexName = "idxFirstName";
break;
case 102:
DMod->NamesTable->IndexName = "idxCompany";
break;
case 103:
DMod->NamesTable->IndexName = "";
break;
}
}
void __fastcall TForm1::IndexClick(TObject *Sender)
{
SetupIndex(Sender);
DMod->NamesTable->FindNearest(OPENARRAY(TVarRec, ("AAAA")));
}
The code has three menu choices for changing the index. The first lets the
user set the index to the last name; the second, to the first name; and the
third, to the company name. All three menu items are attached to the
IndexClick method shown here.
IndexClick calls SetupIndex to do the real work. You use
the tag property of the TMenuItem that is clicked to decide which index
to choose:
switch (dynamic_cast<TComponent *>(Sender)->Tag)
This way, you can call the function with a simple one-line command:
SetupIndex(Sender);
After the index has been set up properly, you can search for the first
relevant record in the database:
DMod->NamesTable->FindNearest(OPENARRAY(TVarRec,
("AAAA")));
The goal of this line is to skip over all the records that contain blanks in
the field on which you're searching. For example, if you switch to the company
index, you might find 20, 100, or even 5,000 records in the table that have no
information in the Company field. To skip over these records, you can
search for the first row that begins with the letter A.
Searching for Records
The program also uses the SetupIndex method when it is conducting
searches. As I stated previously, you can use three possible menu items to start
a search. The first searches on last names; the second, on first names; and the
third, on a company name. I have assigned the same values to the Tag
fields of these TMenuItems that I did to the Tag fields of the
TMenuItems concerned with switching indexes. That way, I can set up the
index properly with a simple call to SetupIndex:
AnsiString IndexName, S;
if (InputQuery("Search for Name", "Enter Name: ", S))
{
IndexName = DMod->NamesTable->IndexName;
SetupIndex(Sender);
DMod->NamesTable->FindNearest(OPENARRAY(TVarRec, (S)));
DMod->NamesTable->IndexName = IndexName;
}
As you can see, the code also saves the current index so that the current
state of the index can be restored after the search:
AnsiString IndexName;
IndexName = DMod->NamesTable->IndexName;
... // Code omitted here
DMod->NamesTable->IndexName = IndexName;
The big point to notice here is how easily you can take care of these chores
by using the VCL. BCB makes database programming easy, even when you're working
with a fairly complex program.
Inserting Data, Canceling Operations
Because this database has five tables, you have to devise a technique for
specifying the name of the table on which you want to perform an insertion,
deletion, or post. I use the TPageControl to handle these chores. In
particular, I assume that if the user is looking at the Address page, then he or
she wants to perform an action on the kdAdds table, and if the user is
looking at the first page, then he or she wants to perform an operation on the
kdNames table, and so on:
void __fastcall TForm1::Insert1Click(TObject *Sender)
{
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1:
DMod->NamesTable->Insert();
break;
case 2:
DMod->AddressTable->Insert();
break;
case 3:
DMod->PhoneTable->Insert();
break;
case 4:
DMod->EMailTable->Insert();
break;
}
}
As you can see, I have set the Tag field for each of the pages to a
unique value so that I can easily determine the current page:
switch
(dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
If the user accidentally makes a wrong decision, he or she can undo the most
recent operation on the currently selected table by clicking Cancel:
void __fastcall TForm1::Cancel1Click(TObject *Sender)
{
AnsiString S;
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1: DMod->NamesTable->Cancel(); break;
case 2: DMod->AddressTable->Cancel(); break;
case 3: DMod->PhoneTable->Cancel(); break;
case 4: DMod->EMailTable->Cancel(); break;
default:
ShowMessage(GetError(1, S));
}
}
This system is easy to implement, but it can be a bit confusing to the user
when he or she is looking at the first page, which holds information about not
only the kdNames table, but also the kdAdds and kdPhone
tables. The issue here is that the database itself won't be much fun if you have
to flip pages to get even the most basic information about a name. To remedy
this problem, I put the Name, Address, and Phone information on the first page
but really expect the user to perform edits on these fields by turning to the
Address or Phone page.
To enforce this rule, you can set up an options menu that can be turned on
and off:
void __fastcall TForm1::SetAddressPhoneClick(TObject
*Sender)
{
SetAddressPhone->Checked = !SetAddressPhone->Checked;
DBEdit6->ReadOnly = SetAddressPhone->Checked;
DBEdit17->ReadOnly = SetAddressPhone->Checked;
DBEdit18->ReadOnly = SetAddressPhone->Checked;
DBEdit19->ReadOnly = SetAddressPhone->Checked;
DBEdit20->ReadOnly = SetAddressPhone->Checked;
DBEdit8->ReadOnly = SetAddressPhone->Checked;
DBEdit17->ReadOnly = SetAddressPhone->Checked;
DBEdit7->ReadOnly = SetAddressPhone->Checked;
}
If you include this code in the program as a response to a TMenuItem
called SetAddressPhone, then the user can decide whether the views on
the kdAdds and kdPhones table will be editable. By default,
this function should be turned off, because it really is better that the user
does not input information in these fields.
I should perhaps add that the user does not have to understand that he or she
is entering data in separate tables. For example, the user doesn't have to know
that the kdAdds and kdPhones tables exist. All he or she has
to understand is that inputting information about phones is a separate operation
from entering data about addresses or names. This much conceptual background the
user must have;
otherwise, he or she will not be able to use the tool at all.
The design of the program helps the user by putting each table on a separate
page. That way, the user can rely on the page metaphor when thinking about the
underlying structure of the database. Providing metaphors for the user is a
useful way to simplify the operation of an application.
-
NOTE: As you might recall, when
I first talked about the Address2 program, I said that for many users, a
simple flat-file database is best. When I said that, I was thinking
particularly about the kinds of problems currently under discussion. Using a
relational database takes a certain amount of conceptual ability that some
users may not have the patience to master. The actual ideas involved are
simple, but many users are still so overwhelmed by the very idea of computers
that they can't clear their heads sufficiently to grasp concepts that would be
easy for them to assimilate in some other field with which they are more
familiar.
It may sound as though I am being overly polite in this note, but I'm trying
to state the facts as I see them. Many intelligent people's minds really do
become inexplicably opaque when it comes to thinking about computers. This
problem will disappear as more and more children grow up using these machines,
but for now programmers have to think seriously every time they add any level
of complexity to their programs. Programmers will understand relational
databases, and so will the small subset of users that are targeted by a
program of this type; but it is important to understand that at this point in
history, many users will find relational databases perhaps too difficult to
understand.
My point is simply that not everyone is ready to work with relational
databases. Some people need them, but others will be confused by them. It is
good to use relational databases when necessary, but programmers should also
be aware that some users might not properly understand them.
Deleting Data: A First Look at the Programs Data
Module
Deleting data is the last topic to be covered before moving over to
examination of the TDataModule for the application. In fact, you will
find that this subject touches on matters that are specific to the program's
data module, so it serves as a good segue into new territory.
Here is the method the main form uses to delete data:
void __fastcall TForm1::Delete1Click(TObject *Sender)
{
AnsiString S;
AnsiString Msg("Are you sure you want to delete \r %s?");
Set<TMsgDlgBtn, 0, 8> Btns;
Btns << mbYes << mbNo;
switch (dynamic_cast<TComponent&>(*PageControl1->ActivePage).Tag)
{
case 1:
Msg = Format(Msg, OPENARRAY(TVarRec,
(DMod->NamesTableFirstLastCompany->AsString)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->CascadingDelete();
break;
case 2:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->Address)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->AddressTable->Delete();
break;
case 3:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->Phone)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->PhoneTable->Delete();
break;
case 4:
Msg = Format(Msg, OPENARRAY(TVarRec, (DMod->EMail)));
if (MessageDlg(Msg, mtInformation, Btns, 0) == ID_YES)
DMod->EMailTable->Delete();
break;
default:
ShowMessage(GetError(1, S));
}
}
As you can see, this code uses the Tag field of the TTabSheet
to determine which table is focused.
The code then pops up a message box asking the user if he or she is sure
about continuing with the deletion. In some cases, you can easily give the user
an intelligent prompt about the contents of the current field:
Msg = Format(Msg, OPENARRAY(TVarRec,
(DMod->NamesTableFirstLastCompany->AsString)));
In this case, the string garnered from one of the fields of the
NamesTable provides all the information the user needs. In fact, the
FirstLast field of the database is a calculated field. This calculated
field consists of combined information from the First, Last,
and Company fields of the kdNames. This combined information
uniquely identifies a record so the user can feel secure when deleting it:
void __fastcall TDMod::NamesTableCalcFields(TDataSet
*DataSet)
{
AnsiString Temp = NamesTableFirstName->Value + " "
+NamesTableLastName->Value;
if (Temp == " ")
NamesTableFirstLastCompany->Value = NamesTableCompany->Value;
else
NamesTableFirstLastCompany->Value = Temp;
}
As you can see, this code combines the first and last names into a single
string. If the string is not empty, it is shown to the user as if it were a
standard field of the database. If the current record has no information in
either the first or last field, then the program assumes that the record must
contain only company information:
NamesTableFirstLastCompany->Value =
NamesTableCompany->Value;
The end result of this system is to show the user records that contain either
someone's first or last name or just a company name. This way, you can ask
the database to perform double duty as both a way of tracking company names and
as a means of tracking the names of people.
This calculated field can be used not only to help with deletions, but also
as an index appearing on the extreme left of the main form, as shown in Figure
14.1. The user will never edit this field directly but will use it as a guide to
all the nearby records in the database. This kind of index is useful if you're
searching for a particular name. For example, I use the database to track the
members of my family. As a result, it has lots of Calverts in it. I can use the
Last Name search to find the section where the Calverts are stored and then use
the index to move back and forth between members of the family.
-
NOTE: You actually have no
guarantee that the string generated by this calculated field will be unique.
The program is designed to make sure the NameCode in the kdNames
table is unique, but nothing in the program prevents you from entering two
identical names, addresses, phone numbers, and so on.
If the user wants to delete an address, you once again need to provide
information from several different fields to identify a record uniquely, as you
can see in Figure 14.7.
FIGURE 14.7. A prompt that uniquely
identifies the address shown in a particular row.
This time, I found it more convenient simply to add to the data module a
method that would return a string uniquely identifying a record:
AnsiString __fastcall TDMod::GetAddress()
{
return DMod->AddressTableAddress1->AsString + `\r' +
DMod->AddressTableAddress2->AsString + `\r' +
DMod->AddressTableCity->AsString + `\r' +
DMod->AddressTableState->AsString + `\r' +
DMod->AddressTableZip->AsString;
}
I thought the most sensible approach was to add a read-only property to the
data
module to aid in retrieving this information:
__property AnsiString Address={read=GetAddress};
You can access this property by writing code that looks like this:
AnsiString S = DMod->Address;
In this particular case, it is arguable that a property doesn't do much for
you other than cover the remote contingency that you might change the parameters
of the GetAddress method. On the other hand, the property doesn't cost
you anything either because the compiler will obviously map any calls to the
Address property directly to the GetAddress method. In other
words, this programming is very cautious because it is unlikely that the
GetAddress method will ever change its spots. However, being conservative
when writing code is almost always best, as long as you're not doing serious
damage to the performance of your program.
I won't bother discussing any of the means for deleting from the other tables
in this program, as they follow the same pattern already established. The key
point to grasp is that you have to show several fields to the user to identify a
record uniquely. Furthermore, placing the burden of generating these strings on
the program's data module is probably best. The reason for doing so is simply
that the generation of these strings is dependent on the structure of the tables
underlying the program.
Isolating all code dependent on these structures inside one object is best so
that you won't have to hunt all over your program to find code that might need
to be modified because of a change in the program's database.
The Data Module: Cascading Deletes
You have already seen that the data module contains special properties that
retrieve strings uniquely identifying certain records. You have also seen the
calculated field that generates a string "uniquely" identifying records from the
kdNames table. What's left to explore are methods that aid in posting
and deleting records.
The issue here is simply that the database contains a number of tables. If
the user wants to delete a name from the database, then he or she is really
asking to not just delete the name, but also the addresses, phone numbers, and
other information associated with that name. This process is known as a
cascading delete.
Delphi provides support for cascading deletes via the referential integrity
dialog found in the Database Desktop. You can see this option in Figure 14.8.
Many databases do not support cascading deletes, so you can implement it on
the client side with just a few lines of code:
void TDMod::CascadingDelete(void)
{
EMailDeleteQuery->ParamByName("NameCode")->AsInteger =
EMailTableNameCode->Value;
EMailDeleteQuery->ExecSQL();
MemoDeleteQuery->ParamByName("NameCode")->AsInteger =
MemoTableNameCode->Value;
MemoDeleteQuery->ExecSQL();
PhoneDeleteQuery->ParamByName("NameCode")->AsInteger =
PhoneTableNameCode->Value;
PhoneDeleteQuery->ExecSQL();
AddressDeleteQuery->ParamByName("NameCode")->AsInteger =
AddressTableNameCode->Value;
AddressDeleteQuery->ExecSQL();
NamesDeleteQuery->ParamByName("NameCode")->AsInteger =
NamesTableNameCode->Value;
NamesDeleteQuery->ExecSQL();
NamesTable->Refresh();
}
FIGURE 14.8. Choose Cascade or Restrict to
get support for cascading deletes in databases that support this feature.
This code looks a bit complicated, in part because some of the lines are long
and need to be wrapped. Underneath, however, its structure is very simple. I
simply walk down the list of tables in the database, accessing the kdNames
table last. I have created a SQL statement for each table that will delete all
the records in the table that have a particular NameCode. For example,
here are the SQL statements for deleting records in the kdNames or
kdAdds tables:
Delete from kdNames where NameCode = :NameCode
Delete from KDAdds where NameCode = :NameCode
As I said, this technology is very simple, and the act implementing cascading
deletes in your application is trivial. The key to the whole process is simply
recognizing that this act is the responsibility of the program's data module.
Then you can create a simple method in the data module to handle the logic of
the operation, and after about five minutes work, you have a method that can be
called from anywhere in your application with a single line of code. (You
should, however, take more than five minutes to test your code against sample
data to make sure that it is working properly.)
-
NOTE: You should use the alias
system built into the BDE to aid in the process of creating sample data
against which you can run tests. In particular, the ideal way to set up this
situation is to include a TDatabase object in your database and then
attach each of your tables and queries to that single TDatabase
object. That way, you can change the alias for the TDatabase object
and thereby globally change the data all your tables are accessing. Needless
to say, you should also make a dummy copy of your data and place it in a
separate database (directory, in Paradox).
I don't use this structure in the current program, but I will in the sections
on InterBase programs that are coming up in the next few chapters. For
example, the Music program from Chapter 16, "Advanced InterBase Concepts,"
uses this structure.
The Data Module: Mass Posts
The opposite problem from deleting records occurs when you have to post the
data in your program. In these cases, you want to be sure that all the data in
all the tables is posted. You wouldn't want to post just the data in the
kdNames table and then leave updates to the kdAddress or
kdPhones tables stranded.
The methods that handle posting the data look like this:
void DoPost(TDataSet *Data)
{
if ((Data->State == dsInsert) || (Data->State == dsEdit))
Data->Post();
}
void TDMod::PostAll(void)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TTable*>(Components[i]))
DoPost(dynamic_cast<TDataSet*>(Components[i]));
}
This code iterates through all the components on the program's data module
looking for TTable objects. When the code finds one, it passes the
object to a method called DoPost that calls the Post method
for the table. The code in DoPost first checks to make sure the table
is in dsInsert or dsEdit mode, as it is an error to call
Post on a table that is in dsBrowse or some other mode where a
Post can't occur.
Notice that I use the ComponentCount property of TDataModule
to determine how many components I need to check. I then call dynamic_cast
to check whether it is safe to assume the current component is a TTable.
The preceding code is actually a bit wasteful of clock cycles, in that it is not
necessary to support the overhead of a dynamic_cast after you're sure
the cast will succeed:
void TDMod::PostAll(void)
{
int i;
for (i = 0; i < ComponentCount; i++)
if (dynamic_cast<TTable*>(Components[i]))
DoPost((TDataSet*)(Components[i]));
}
In this case, the first cast has additional overhead but is safe, in that no
exceptions will be raised if it fails. The second cast is then guaranteed to
succeed, so you can ask the compiler to do it at compile time rather than
generate runtime code:
DoPost((TDataSet*)(Components[i]));
Putting Error Strings in String Resources
The other subject worth touching on briefly in regard to this program
involves the matter of using string resources to handle error strings. The
program has a very small string resource that contains only one string:
#include "kderrs.inc"
STRINGTABLE
{
KDERR_CASESTATEMENT, "Command fell through case statement"
}
In a program used in the real world, you would probably want to generate many
more error strings.
You can use the following method to retrieve error strings from the program's
resource:
#define ERR_STRING_SIZE 255
AnsiString GetError(int ErrNo, AnsiString &S)
{
S.SetLength(ERR_STRING_SIZE);
LoadString(HINSTANCE(HInstance), 1, S.c_str(), ERR_STRING_SIZE);
return S;
}
This code calls the Windows API routine called LoadString to do the
actual grunt work. Several built-in VCL routines also provide this same
functionality. Notice that an include file defines the
KDERR_CASESTEMENT constant:
#define KDERR_CASESTATEMENT 1
That's all I'm going to say about this error-handling code. If you want more
information, you can refer to the discussion of string resources at the end of
Chapter 5, "Exceptions."
The About Box
For the sake of completeness, I will wrap up the examination of this program
by showing you the About box for the program, shown in Figure 14.9. You can
generate About boxes automatically in BCB by choosing File | New, turning to the
Forms page of the Object Repository, and choosing About Box, as shown in Figure
14.10. I choose to Copy the code rather than to Inherit or Use it, as you can
see in the bottom left of Figure 14.10. Proceeding this way is usually the best
and simplest method unless you're sure you have mastered the technology involved
with form inheritance.
FIGURE 14.9. The About box for the kdAdds
program.
FIGURE 14.10. Creating an About box from the
Object Repository.
You've now come to the end of the discussion of the kdAdds program. The rest
of the chapter consists of a discussion of two utilities associated with this
program. One will transfer data from the Address2 program to the kdAdds program,
and the second will show how to use the TSession object to explore the
structure of the tables used in the kdAdds program.
Using the kdAddExplore Program
The kdAddExplore program uses the TSession object to explore the
tables used in the kdAdds program. The TSession object is often
overlooked by VCL programmers because its operation is usually handled behind
the scenes without need
for intervention. However, if you want to explore the structure of a database
at runtime, this is the object to use. In fact, this object might have been more
usefully called the TDataExplorer object rather than TSession.
-
NOTE: The TSession is
created automatically by an application on startup. If you're working inside a
DLL or in a console mode application, then no default TApplication
object exists to start up a session. As a result, you may have to create your
own TSession object or else call the Initialize method of a
TApplication object. Otherwise, you will not be able to use databases
inside your program.
Before you begin the technical part of this chapter, spending a few moments
running the kdAddExplore program found on disk might be helpful. This program
demonstrates techniques of examining the structure of an existing database.
-
NOTE: The kdAddExplore program
bears a close resemblance to a cut-down version of the Database Explorer
(DBX). There is nothing coincidental about this similarity. However, I have
never seen the source to DBX nor discussed its structure with its author. My
intention here is not to create a substitute for the DBX, but only to provide
a simple means of showing the reader how to explore database objects at
runtime.
You can use this kind of information to provide utilities for your users or
merely to extend your own knowledge of the BDE and VCL. The code also provides
an example of how to use the TTreeView object.
Throughout the rest of this chapter, I will use a global TSession
object created automatically whenever you include database code in your
programs. However, you also can drop down a TSession component on your
forms if you would like to look at a visual object. I do not use it here because
it would bring no additional functionality to my program. However, you might
want to at least view the TSession component and take a look at its
password-related properties.
Working with TSession
TSession is used to manage all the database connections within a
session. It is a global object that wraps up not only TTable and
TQuery, but also TDatabase. A single TSession object
might manage many tables, queries, and databases.
The TSession object has two sets of methods. The first has to do
with
managing a session. The methods encompassing this set of functionality are
shown in Table 14.12.
Table 14.12. The session management routines from the TSession object.
| Routine |
Description |
| Close |
Closes all databases |
| CloseDatabase |
Closes a particular database |
| Open |
Opens the session: Active = True; |
| OpenDatabase |
Opens a specific database |
| AddPassword |
Creates a password for the session |
| RemovePassword |
Deletes a password |
| RemoveAllPasswords |
Clears the password list |
| DropConnections |
Closes all currently inactive databases and datasets |
The second set of routines found in TSession are the methods that are
of interest in the current context of this book. These routines are shown in
Table 14.13.
Table 14.13. Routines for querying a session regarding the
available databases, tables, drivers, and stored procedures.
| Routine |
Description |
| GetAliasNames |
Gets the list of BDE aliases for a database |
| GetAliasParams |
Gets the list of parameters for a BDE alias |
| GetAliasDriverName |
Gets the BDE driver for an alias of a database |
| GetDatabaseNames |
Gets a list of BDE aliases and TDatabase objects |
| GetDriverNames |
Gets the names of installed BDE drivers |
| GetDriverParams |
Gets parameters for a BDE driver |
| GetTableNames |
Gets tables associated with a database |
| GetStoredProcNames |
Gets stored procedures for a database |
Routines such as GetDatabaseNames and GetTableNames can
retrieve a list of all the available databases and tables on the current system.
You can see this data on display inside the kdAddExplore program. For example,
all the databases in my system at the time of this writing are visible in the
main screen of the kdAddExplore program, as shown in Figure 14.11.
You can open up the nodes of the kdAddExplore program to see a list of all
the tables in a particular database, as shown in Figure 14.12. You can then
drill down even further to the names of the fields and indexes in a particular
table. Finally, you can even see the names of the fields involved in a
particular index, as shown in Figure 14.13.
Another set of TSession functionality tapped into by the
kdAddExplore program involves looking at the alias found on a system. You can
drill down in this dialog to see the parameters passed to a particular alias, as
shown in Figure 14.14.
FIGURE 14.11. The kdAddExplore program
displays all the available databases on my system.
FIGURE 14.12. The tables in the
CUnleashed database, which holds most of the data used in this book.
FIGURE 14.13. The fields and indexes on the
kdNames table. Notice that you can drill down to see the fields in each
index.
FIGURE 14.14. The kdAddExplore program shows
all the aliases on the system. The open branch is from an InterBase database.
The Code for the kdAddExplore Program
Most of the complexity in the kdAddExplore program comes from manipulating
the TTreeView object. The code for querying the TSession
object is fairly straightforward in most cases; the TTreeView makes the
code a bit tricky in places. The source for this program is shown in Listings
14.9 through 14.15.
Listing 14.9. The header file for the main module in
the kdAddExplore program.
///////////////////////////////////////
// FILE: Main.h
// PROJECT: KdAddExplore
// Copyright (c) 1997 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\ComCtrls.hpp>
#include <vcl\Menus.hpp>
class TDBNames : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
TTreeView *TView;
TImageList *ImageList1;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *AliasView1;
TMenuItem *N1;
TMenuItem *Exit1;
void __fastcall FormShow(TObject *Sender);
void __fastcall TViewExpanding(TObject *Sender, TTreeNode *Node,
Boolean &AllowExpansion);
void __fastcall Exit1Click(TObject *Sender);
void __fastcall AliasView1Click(TObject *Sender);
private: // User declarations
void DeleteTemp(TTreeNode *Node);
void AddTables(TTreeNode *Node);
void FindFieldsAndIndices(TTreeNode *Node);
void FindFields(TTreeNode *Node, TTable *Table);
void FindIndices(TTreeNode *Node, TTable *Table);
public: // User declarations
virtual __fastcall TDBNames(TComponent* Owner);
};
extern TDBNames *DBNames;
#endif
Listing 14.10. The main module in the kdAddExplore
program.
///////////////////////////////////////
// FILE: MAIN.CPP
// PROJECT: KDADDEXPLORER
// Copyright (c) 1997 Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "AliasView.h"
#pragma resource "*.dfm"
#pragma resource "image.res"
TDBNames *DBNames;
__fastcall TDBNames::TDBNames(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TDBNames::FormShow(TObject *Sender)
{
int i;
AnsiString S;
TStringList *DBNamesList = new TStringList;
TTreeNode *Node;
ImageList1->ResourceLoad(rtBitmap, "FolderShut", clPurple);
Session->GetDatabaseNames(DBNamesList);
for (i = 0; i < DBNamesList->Count; i++)
{
S = DBNamesList->Strings[i];
Node = TView->Items->Add(TView->Selected, S);
TView->Items->AddChild(Node, "TEMP");
}
DBNamesList->Free();
}
void TDBNames::DeleteTemp(TTreeNode *Node)
{
TTreeNode *TempNode;
if (Node->Count == 1)
{
TempNode = Node->getFirstChild();
if (TempNode->Text == "TEMP")
TempNode->Delete();
}
}
void TDBNames::AddTables(TTreeNode *Node)
{
int j;
AnsiString S(Node->Text);
TTreeNode *TempNode, *ChildNode;
TStringList *List = new TStringList;
DeleteTemp(Node);
Session->GetTableNames(S, "*.*", True, False, List);
for (j = 0; j < List->Count; j++)
{
TempNode = TView->Items->AddChild(Node, List->Strings[j]);
ChildNode = TView->Items->AddChild(TempNode, "Fields");
TView->Items->AddChild(ChildNode, "TEMP");
ChildNode = TView->Items->AddChild(TempNode, "IndexNames");
TView->Items->AddChild(ChildNode, "TEMP");
}
List->Free();
}
void TDBNames::FindFields(TTreeNode *Node, TTable *Table)
{
int i;
for (i = 0; i < Table->FieldCount; i++)
TView->Items->AddChild(Node, Table->Fields[i]->FieldName);
}
void TDBNames::FindIndices(TTreeNode *Node, TTable *Table)
{
int i;
TStringList *List = new TStringList;
TTreeNode *ChildNode;
AnsiString S;
Table->GetIndexNames(List);
TIndexDefs *IndexDefs = Table->IndexDefs;
for (i = 0; i < IndexDefs->Count; i++)
{
S = IndexDefs->Items[i]->Name;
if (S.Length() == 0)
S = "Primary";
ChildNode = TView->Items->AddChild(Node, S);
S = IndexDefs->Items[i]->Fields;
TView->Items->AddChild(ChildNode, S);
}
List->Free();
}
void TDBNames::FindFieldsAndIndices(TTreeNode *Node)
{
TTable *Table = new TTable(this);
Table->DatabaseName = Node->Parent->Parent->Text;
Table->TableName = Node->Parent->Text;
Table->Open();
DeleteTemp(Node);
if (Node->Count < 1)
switch (Node->Index)
{
case 0:
FindFields(Node, Table);
break;
case 1:
FindIndices(Node, Table);
break;
}
Table->Free();
}
void __fastcall TDBNames::TViewExpanding(TObject *Sender, TTreeNode *Node,
Boolean &AllowExpansion)
{
switch (Node->Level)
{
case 0:
if (Node->Count <= 1)
AddTables(Node);
break;
case 2:
FindFieldsAndIndices(Node);
break;
}
}
void __fastcall TDBNames::Exit1Click(TObject *Sender)
{
Close();
}
void __fastcall TDBNames::AliasView1Click(TObject *Sender)
{
AliasForm->Show();
}
Listing 14.11. The header for the unit that displays
information on aliases.
///////////////////////////////////////
// FILE: AliasView.h
// PROJECT: KDADDEXPLORER
// Copyright (c) 1997 Charlie Calvert
//
#ifndef AliasViewH
#define AliasViewH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
#include <vcl\Menus.hpp>
class TAliasForm : public TForm
{
__published:
TListBox *ListBox1;
TListBox *ListBox2;
void __fastcall ListBox1Click(TObject *Sender);
void __fastcall FormShow(TObject *Sender);
private:
public:
virtual __fastcall TAliasForm(TComponent* Owner);
};
extern TAliasForm *AliasForm;
#endif
Listing 14.12. The main source file of the unit that
displays information about the aliases on the system.
///////////////////////////////////////
// FILE: AliasView.cpp
// PROJECT: KDADDEXPLORER
// Copyright (c) 1997 Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "AliasView.h"
#pragma resource "*.dfm"
TAliasForm *AliasForm;
__fastcall TAliasForm::TAliasForm(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TAliasForm::ListBox1Click(TObject *Sender)
{
AnsiString S(ListBox1->Items->Strings[ListBox1->ItemIndex]);
Session->GetAliasParams(S, ListBox2->Items);
}
void __fastcall TAliasForm::FormShow(TObject *Sender)
{
Session->GetAliasNames(ListBox1->Items);
}
Listing 14.13. The header file for the programs data
module.
///////////////////////////////////////
// FILE: DMOD1.h
// PROJECT: KDADDEXPLORER
// Copyright (c) 1997 Charlie Calvert
//
#ifndef DMod1H
#define DMod1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DB.hpp>
#include <vcl\DBTables.hpp>
class TDMod : public TDataModule
{
__published:
TDatabase *Database1;
private:
public:
virtual __fastcall TDMod(TComponent* Owner);
};
extern TDMod *DMod;
//----------------------------------
#endif
Listing 14.14. The data module for the main program
contains a TDatabase object but no custom code.
///////////////////////////////////////
// FILE: DMOD1.CPP
// PROJECT: KDADDEXPLORER
// Copyright (c) 1997 Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
}
Listing 14.15. The custom resource file holding the
bitmap displayed in the TTreeView.
FolderShut BITMAP "FLDRSHUT.BMP"
Using a TTreeView to Display the Databases on a System
When the kdAddExplore program is launched, it first iterates through the
available databases on the system and displays to the user in a TTreeView:
void __fastcall TDBNames::FormShow(TObject *Sender)
{
int i;
AnsiString S;
TStringList *DBNamesList = new TStringList;
TTreeNode *Node;
ImageList1->ResourceLoad(rtBitmap, "FolderShut", clPurple);
Session->GetDatabaseNames(DBNamesList);
for (i = 0; i < DBNamesList->Count; i++)
{
S = DBNamesList->Strings[i];
Node = TView->Items->Add(TView->Selected, S);
TView->Items->AddChild(Node, "TEMP");
}
DBNamesList->Free();
}
This code needs to have an icon that it can use to spruce up the nodes of the
TTreeView. It stores the bitmap used as an icon in a resource file that
looks like this:
FolderShut BITMAP "FLDRSHUT.BMP"
FLDRSHUT.BMP is one of the files that ships in the Images
subdirectory that is created when you install BCB.
The code loads the bitmap into a TImageList object, which is
associated with the TTreeView object via the Images property.
You can view the Images property of the TTreeView object in
the Object Inspector, and you can use a drop-down list to select the
TImageList you want to use with this component. Because only one image
appears in this image list, it will automatically be associated with all the
nodes of the TTreeView object. In this particular case, that is a
satisfactory solution to the problem of how to give some visual interest to the
object.
After getting the icon set up, the program retrieves the list of available
aliases from the TSession object and stores them inside a
TStringList:
TStringList *DBNamesList = new TStringList;
Session->GetDatabaseNames(DBNamesList);
After you have the list of items, you can easily store each one inside a
TTreeNode object that can be hung on the TTreeView for display to
the user:
TTreeNode *Node;
for (i = 0; i < DBNamesList->Count; i++)
{
S = DBNamesList->Strings[i];
Node = TView->Items->Add(TView->Selected, S);
TView->Items->AddChild(Node, "TEMP");
}
Clearly, the TTreeNode object is the key to working with
TTreeViews. This object represents an individual node on a TTreeView.
It encapsulates a bitmap and a caption, and can be identified by a unique index
number.
Notice that I call two methods of the Items property of the
TTreeView object, which in this program is called TView; it is not
a type, but an abbreviation for TreeView. The first call adds the name
of the database as a node. The next call adds a child to that database node
containing a string consisting of the word "Temp". The "Temp"
node is never shown to the user but exists only to force the TTreeView
to display a plus sign indicating to the user that the node can be expanded
further. When it comes time to expand the node, I delete the word Temp
and substitute a word that actually displays the name of the one of tables in
the database.
The use of the "Temp" node may seem like a nasty kluge at first, but
it really is more intelligent to do things this way rather than force the user
to sit still while I open all the databases, including those that might need a
password, and find all the tables inside them. When you think of things from
this perspective, adding a temporary node to each item in the tree suddenly
seems very logical. If the user wants to expand a particular node, then you can
retrieve detailed information about that particular database. This approach is
much better than trying to retrieve information about every table on the system.
Expanding the Nodes of the TTreeView
The program must respond appropriately when the user clicks on a node of the
TTreeView object. In particular, if the user is first opening a
particular database node, then the code needs to retrieve the list of tables in
that database and display them to the user. If the user clicks one of the
tables, then a list of fields and indexes must be retrieved, and so on.
An OnExpanding event gets called automatically when the user wants
to open a node. Here is how the kdAddExplore program responds to this event:
void __fastcall TDBNames::TViewExpanding(TObject
*Sender, TTreeNode *Node,
Boolean &AllowExpansion)
{
switch (Node->Level)
{
case 0:
if (Node->Count <= 1)
AddTables(Node);
break;
case 2:
FindFieldsAndIndices(Node);
break;
}
}
As you can see, the program calls a method named AddTables if the
user is working at the first level of the tree, and it calls a method called
FindFieldsAndIndices if the user is working at the second level of the
tree. The level the user is currently exploring appears in the Level
field of the TTreeNode passed to the OnExpanding event
handler.
Before calling AddTables, I check to see if more than one child node
already appears on this particular node of the TTreeView. If more than
one node exists, then I assume that the database has already been explored and
that the node can be opened without any further querying of the system. If only
one node exists, then I assume that this is the "Temp" node created in
the program's OnShow event, and I call AddTables so that the
node can be updated.
Adding a List of Available Tables to the
TTreeView
Here is the code that is called when it's time to explore the tables on the
system:
void TDBNames::DeleteTemp(TTreeNode *Node)
{
TTreeNode *TempNode;
if (Node->Count == 1)
{
TempNode = Node->getFirstChild();
if (TempNode->Text == "TEMP")
TempNode->Delete();
}
}
void TDBNames::AddTables(TTreeNode *Node)
{
int j;
AnsiString S(Node->Text);
TTreeNode *TempNode, *ChildNode;
TStringList *List = new TStringList;
DeleteTemp(Node);
Session->GetTableNames(S, "*.*", True, False, List);
for (j = 0; j < List->Count; j++)
{
TempNode = TView->Items->AddChild(Node, List->Strings[j]);
ChildNode = TView->Items->AddChild(TempNode, "Fields");
TView->Items->AddChild(ChildNode, "TEMP");
ChildNode = TView->Items->AddChild(TempNode, "IndexNames");
TView->Items->AddChild(ChildNode, "TEMP");
}
List->Free();
}
The first method shown here, called DeleteTemp, is used to delete
the "Temp" nodes created in the OnShow event. The code checks
to make sure the string is actually set to "Temp" just to be sure that
I haven't stumbled across a database that has only one table in it. The program
would, of course, behave badly if it encountered a database with a single table
in it called "Temp"!
The next step is for the program to retrieve the list of tables in a database
from the Session object:
Session->GetTableNames(S, "*.*", True, False, List);
The code uses the string name from the node passed to the OnExpanded
event to query TSession for the proper set of tables. You can look up
GetTableNames in the online help for detailed explanation of this call,
but most readers should be able to figure out what is going on from this
declaration:
void __fastcall GetTableNames(
const System::AnsiString DatabaseName,
const System::AnsiString Pattern,
bool Extensions,
bool SystemTables,
Classes::TStrings* List);
Set Extensions to True if you want to retrieve the
extension for a dBASE or Paradox table. Also set Extensions to True
if you want to retrieve system tables for SQL databases such as InterBase.
The program is at last ready to add the tables to the TTreeView:
TempNode = TView->Items->AddChild(Node,
List->Strings[j]);
ChildNode = TView->Items->AddChild(TempNode, "Fields");
TView->Items->AddChild(ChildNode, "TEMP");
ChildNode = TView->Items->AddChild(TempNode, "IndexNames");
TView->Items->AddChild(ChildNode, "TEMP");
This code first adds a table name to the TTreeView:
TempNode = TView->Items->AddChild(Node,
List->Strings[j]);
It then adds two child nodes labeled Fields and IndexNames
to the Table Name. Once again, I resort to the trick of placing a "Temp"
node under these two fields to indicate to the user that the nodes can be
expanded further. However, I do not actually expand the nodes at this time
because the user may not ever want to see the data in question.
Finding Out About Indexes and Fields
To find out about indexes and fields, I abandon the TSession object
and instead create a TTable object, since that is the object that can
give me the information I need:
void TDBNames::FindFieldsAndIndices(TTreeNode *Node)
{
TTable *Table = new TTable(this);
Table->DatabaseName = Node->Parent->Parent->Text;
Table->TableName = Node->Parent->Text;
Table->Open();
DeleteTemp(Node);
if (Node->Count < 1)
switch (Node->Index)
{
case 0:
FindFields(Node, Table);
break;
case 1:
FindIndices(Node, Table);
break;
}
Table->Free();
}
The program first queries TTreeView to retrieve the name of the
database the user wants to explore and the name of the particular table under
examination:
Table->DatabaseName = Node->Parent->Parent->Text;
Table->TableName = Node->Parent->Text;
The table is then opened, and the "Temp" node associated with it is
deleted:
Table->Open();
DeleteTemp(Node);
I hung the nodes with the labels Fields and IndexNames in a
particular order, so I can use the Index field of the current Node
to know when to retrieve information on fields and when to retrieve information
on indexes:
switch (Node->Index)
{
case 0:
FindFields(Node, Table);
break;
case 1:
FindIndices(Node, Table);
break;
}
The FindFields method is very simple, in large part because it is a
leaf node on the tree and does not need to be expanded further:
void TDBNames::FindFields(TTreeNode *Node, TTable
*Table)
{
int i;
for (i = 0; i < Table->FieldCount; i++)
TView->Items->AddChild(Node, Table->Fields[i]->FieldName);
}
I have to do a little coaxing to get the system to give up information on
indexes:
void TDBNames::FindIndices(TTreeNode *Node, TTable
*Table)
{
int i;
TStringList *List = new TStringList;
TTreeNode *ChildNode;
AnsiString S;
Table->GetIndexNames(List);
TIndexDefs *IndexDefs = Table->IndexDefs;
for (i = 0; i < IndexDefs->Count; i++)
{
S = IndexDefs->Items[i]->Name;
if (S.Length() == 0)
S = "Primary";
ChildNode = TView->Items->AddChild(Node, S);
S = IndexDefs->Items[i]->Fields;
TView->Items->AddChild(ChildNode, S);
}
List->Free();
}
I first get the list of index names from the TTable object and then
retrieve the relevant TIndexDefs object. This object contains
information on a particular index. I iterated through the Items in the
IndexDefs and displayed the information to the user.
You might think that I would have to have a second loop inside the first loop
to handle the case in which an index consists of more than one field. However, a
second loop is not necessary, because the list is sent to me in the form of a
single string, with each index delimited by a semicolon. For example, the
primary index of the Items table from BCDEMOS consists of two
fields. This information is displayed by TIndexDefs as follows:
OrderNo;ItemNo
Displaying Aliases and Alias Parameters
After all the work involved with displaying information about databases,
tables, indexes, and fields, you will find that querying the system about
aliases is relatively trivial. One of the main reasons this process is so much
simpler is that I use list boxes rather than a TTreeView to display
information. TTreeViews are great for the user, but not much fun for
the programmer!
Here is the custom code from the unit that displays the alias to the user.
All the other code in the unit is generated by the system:
void __fastcall TAliasForm::ListBox1Click(TObject
*Sender)
{
AnsiString S(ListBox1->Items->Strings[ListBox1->ItemIndex]);
Session->GetAliasParams(S, ListBox2->Items);
}
void __fastcall TAliasForm::FormShow(TObject *Sender)
{
Session->GetAliasNames(ListBox1->Items);
}
The program opts to display this information in a separate form, rather than
overlaying it on top of the information about databases. This form has two list
boxes in it. The first list box holds the various aliases available on the
system, and the second list box holds the parameters for the currently selected
alias.
When the form is first shown, I call the GetAliasNames method of the
global Session object and then pass it the TStrings-based
property of TListBox. That's all I need to do to show the user the
aliases!
If the user selects a particular item in the first list box, then the
ListBox1Click event handler is called. This code initializes a string to
the name of the currently selected alias:
AnsiString
S(ListBox1->Items->Strings[ListBox1->ItemIndex]);
Then it queries the Session object for the list of parameters
associated with that object:
Session->GetAliasParams(S, ListBox2->Items);
As you can see, this second list is displayed in the second list box, as
shown in Figure 14.14.
Data Migration, Temporary Tables, NULL Fields
On the CD that comes with this book, in the Chapter 14 directory,
you will find a program that transfers the data from the Address2 program to the
kdAdds program. By and large, this code is too mundane to bother including in
the text of this book. However, migration poses a common problem, and one that
you may need to tackle. If so, you can examine the program.
A TBatchMove component is on the Data Access page of the Component
Palette. This object can be used to move data quickly between two tables. For
example, it can be very helpful when you want to move data from an abstract
dataset to a temporary table that is saved to disk. For example, Paradox is
always creating temporary tables called Answer.db. If you want to
create tables of that type, then use the TBatchMove component.
Another powerful tool that ships with BCB is the Data Migration Expert. This
tool can help you migrate data from one database to another. For example, if you
want
to move a database from Paradox to InterBase, then you can use the Data
Migration Expert. This tool will re- create all the fields, the aliases, and
even the referential integrity that you have built into your first database.
Needless to say, no tool
of this type is going to be perfect, due the nature of the wide variety of
databases available. However, I have had surprisingly good luck with it and have
found that it can save me hours, if not days, of work.
The particular problem I deal with in this case cannot be aided much by the
kinds of tools mentioned in the preceding few paragraphs. The reason for this
failure is simply that I need to completely restructure the database used in
Address2 before it can be used in kdAdds. In particular, I need to break up the
one table used in Address2 into five tables used in the relational model that
underlies kdAdds. I cannot automate that kind of process using any tools that
ship with BCB, so I do it by hand in the program called AddressToKd (also found
on the CD).
The only code from the AddressToKd program that I want to show you in this
text is the code that checks to see whether a field is set to NULL:
void TDMod::TransferEMail()
{
if (!AddressTableEMail1->IsNull)
{
KDEMailTable->Insert();
KDEMailTableAddress->AsString = AddressTableEMail1->AsString;
KDEMailTable->Post();
}
if (!AddressTableEMail2->IsNull)
{
KDEMailTable->Insert();
KDEMailTableAddress->AsString = AddressTableEMail2->AsString;
KDEMailTable->Post();
}
}
This code checks to see whether the Email1 and Email2
fields of the Address table from the Address2 program are set to NULL.
This process is necessary because trying to access a field that has no data in
it whatsoever is sometimes not a good idea. Because you, too, might run across a
situation in which you need to do this, I have included the code here. As you
can see, all you need to do is call the IsNull method of the TField
object. This method returns True if the field is set to NULL,
and it returns False
otherwise. A NULL field is a blank or empty field that contains no
data.
After I'm sure that it's safe to work with the field, I insert a record into
the kdEMail table, add the e-mail information to it, and post the
record. That's all I'm going to say about the program here in the text of this
book, but you can open it up in BCB and explore it at your leisure if you are
interested in it.
Summary
In this chapter, you have had a look at relational databases. The core
material was divided into two sections. The first section looked at a simple
relational database program consisting of five interrelated tables. You saw how
these tables are tied together and how to add, delete, insert, and edit records
in these tables. Also included is a relatively lengthy discussion of the indexes
and keys in the table and of why they were created. Other subjects include
searching and storing strings in a string
table.
The second half of the chapter was dedicated to an examination of the global
TSession object that is created automatically whenever you use the BDE
database tools in your program. You saw that you can use this object to query
the system about the aliases, databases, and tables it is using. You also saw
how to query a TTable object about its fields and indexes.
Other information included in this chapter relates mostly to using standard
BCB components such as the TTreeView. You saw that the TTreeView
object is very powerful, and it allows you to display information in a way that
the user can easily comprehend. Several portions of the chapter focus on the
TTreeNode object used to fill in the nodes of a TTreeView. In
particular, you saw how to add child nodes to a TTreeView.
|