Case Study 1: Building an Address Book
Overview
The address book is a classic desktop application that exercises nearly every concept from this chapter: multiple controls for data entry, a grid for displaying contacts, multiple forms for add/edit operations, validation, search, and layout management. We build it step by step, making deliberate design decisions along the way.
Problem Statement
Build an address book application with the following features:
- A main form showing all contacts in a TStringGrid (Name, Phone, Email, City).
- An "Add Contact" button that opens a modal form for entering new contact details.
- An "Edit" button that opens the same modal form pre-populated with the selected contact.
- A "Delete" button that removes the selected contact after confirmation.
- A search box (TEdit) at the top that filters the grid in real time as the user types.
- The main grid should highlight the selected row and support column resizing.
Application Architecture
Data Layer
We store contacts in a simple dynamic array of records, keeping business logic separate from the UI:
type
TContact = record
Name: string;
Phone: string;
Email: string;
City: string;
Notes: string;
end;
var
Contacts: array of TContact;
ContactCount: Integer;
procedure AddContact(const C: TContact);
function FindContacts(const SearchTerm: string): array of Integer;
procedure DeleteContact(Index: Integer);
Main Form
unit AddressBookMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, Grids,
StdCtrls, ExtCtrls;
type
{ TfrmAddressBook }
TfrmAddressBook = class(TForm)
pnlTop: TPanel;
lblSearch: TLabel;
edtSearch: TEdit;
pnlButtons: TPanel;
btnAdd: TButton;
btnEdit: TButton;
btnDelete: TButton;
sgContacts: TStringGrid;
procedure btnAddClick(Sender: TObject);
procedure btnEditClick(Sender: TObject);
procedure btnDeleteClick(Sender: TObject);
procedure edtSearchChange(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure sgContactsDblClick(Sender: TObject);
private
FContacts: array of TContact;
FFilteredIndices: array of Integer;
procedure RefreshGrid;
procedure ApplyFilter(const SearchText: string);
function GetSelectedContactIndex: Integer;
public
end;
implementation
uses ContactDetailUnit;
{$R *.lfm}
procedure TfrmAddressBook.FormCreate(Sender: TObject);
begin
Caption := 'Address Book';
Width := 750;
Height := 500;
Position := poScreenCenter;
{ Grid setup }
sgContacts.ColCount := 4;
sgContacts.RowCount := 1;
sgContacts.FixedRows := 1;
sgContacts.FixedCols := 0;
sgContacts.Options := sgContacts.Options
+ [goRowHighlight, goColSizing] - [goEditing, goRangeSelect];
sgContacts.Cells[0, 0] := 'Name';
sgContacts.Cells[1, 0] := 'Phone';
sgContacts.Cells[2, 0] := 'Email';
sgContacts.Cells[3, 0] := 'City';
sgContacts.ColWidths[0] := 180;
sgContacts.ColWidths[1] := 140;
sgContacts.ColWidths[2] := 200;
sgContacts.ColWidths[3] := 140;
{ Layout }
pnlTop.Align := alTop;
pnlTop.Height := 40;
pnlButtons.Align := alBottom;
pnlButtons.Height := 45;
sgContacts.Align := alClient;
sgContacts.BorderSpacing.Around := 4;
{ Add some sample data }
SetLength(FContacts, 3);
FContacts[0].Name := 'Rosa Martinelli';
FContacts[0].Phone := '555-0101';
FContacts[0].Email := 'rosa@example.com';
FContacts[0].City := 'Portland';
FContacts[1].Name := 'Tomas Vieira';
FContacts[1].Phone := '555-0202';
FContacts[1].Email := 'tomas@example.com';
FContacts[1].City := 'Austin';
FContacts[2].Name := 'Kenji Nakamura';
FContacts[2].Phone := '555-0303';
FContacts[2].Email := 'kenji@example.com';
FContacts[2].City := 'Seattle';
RefreshGrid;
end;
procedure TfrmAddressBook.RefreshGrid;
var
I: Integer;
begin
if Length(FFilteredIndices) = 0 then
begin
{ Show all contacts }
SetLength(FFilteredIndices, Length(FContacts));
for I := 0 to High(FContacts) do
FFilteredIndices[I] := I;
end;
sgContacts.RowCount := Length(FFilteredIndices) + 1;
for I := 0 to High(FFilteredIndices) do
begin
sgContacts.Cells[0, I + 1] := FContacts[FFilteredIndices[I]].Name;
sgContacts.Cells[1, I + 1] := FContacts[FFilteredIndices[I]].Phone;
sgContacts.Cells[2, I + 1] := FContacts[FFilteredIndices[I]].Email;
sgContacts.Cells[3, I + 1] := FContacts[FFilteredIndices[I]].City;
end;
end;
procedure TfrmAddressBook.ApplyFilter(const SearchText: string);
var
I, Count: Integer;
LowerSearch: string;
begin
if SearchText = '' then
begin
SetLength(FFilteredIndices, Length(FContacts));
for I := 0 to High(FContacts) do
FFilteredIndices[I] := I;
end
else
begin
LowerSearch := LowerCase(SearchText);
SetLength(FFilteredIndices, Length(FContacts));
Count := 0;
for I := 0 to High(FContacts) do
begin
if (Pos(LowerSearch, LowerCase(FContacts[I].Name)) > 0) or
(Pos(LowerSearch, LowerCase(FContacts[I].Email)) > 0) or
(Pos(LowerSearch, LowerCase(FContacts[I].City)) > 0) or
(Pos(LowerSearch, LowerCase(FContacts[I].Phone)) > 0) then
begin
FFilteredIndices[Count] := I;
Inc(Count);
end;
end;
SetLength(FFilteredIndices, Count);
end;
RefreshGrid;
end;
procedure TfrmAddressBook.edtSearchChange(Sender: TObject);
begin
ApplyFilter(edtSearch.Text);
end;
function TfrmAddressBook.GetSelectedContactIndex: Integer;
var
GridRow: Integer;
begin
GridRow := sgContacts.Row;
if (GridRow >= 1) and (GridRow - 1 <= High(FFilteredIndices)) then
Result := FFilteredIndices[GridRow - 1]
else
Result := -1;
end;
procedure TfrmAddressBook.btnAddClick(Sender: TObject);
var
DetailForm: TfrmContactDetail;
NewContact: TContact;
begin
DetailForm := TfrmContactDetail.Create(Self);
try
DetailForm.Caption := 'Add Contact';
if DetailForm.ShowModal = mrOK then
begin
NewContact.Name := DetailForm.ContactName;
NewContact.Phone := DetailForm.Phone;
NewContact.Email := DetailForm.Email;
NewContact.City := DetailForm.City;
NewContact.Notes := DetailForm.Notes;
SetLength(FContacts, Length(FContacts) + 1);
FContacts[High(FContacts)] := NewContact;
ApplyFilter(edtSearch.Text);
end;
finally
DetailForm.Free;
end;
end;
procedure TfrmAddressBook.btnEditClick(Sender: TObject);
var
Idx: Integer;
DetailForm: TfrmContactDetail;
begin
Idx := GetSelectedContactIndex;
if Idx < 0 then
begin
ShowMessage('Please select a contact to edit.');
Exit;
end;
DetailForm := TfrmContactDetail.Create(Self);
try
DetailForm.Caption := 'Edit Contact';
DetailForm.ContactName := FContacts[Idx].Name;
DetailForm.Phone := FContacts[Idx].Phone;
DetailForm.Email := FContacts[Idx].Email;
DetailForm.City := FContacts[Idx].City;
DetailForm.Notes := FContacts[Idx].Notes;
if DetailForm.ShowModal = mrOK then
begin
FContacts[Idx].Name := DetailForm.ContactName;
FContacts[Idx].Phone := DetailForm.Phone;
FContacts[Idx].Email := DetailForm.Email;
FContacts[Idx].City := DetailForm.City;
FContacts[Idx].Notes := DetailForm.Notes;
ApplyFilter(edtSearch.Text);
end;
finally
DetailForm.Free;
end;
end;
procedure TfrmAddressBook.btnDeleteClick(Sender: TObject);
var
Idx, I: Integer;
begin
Idx := GetSelectedContactIndex;
if Idx < 0 then
begin
ShowMessage('Please select a contact to delete.');
Exit;
end;
if MessageDlg('Confirm Delete',
Format('Delete contact "%s"?', [FContacts[Idx].Name]),
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
for I := Idx to High(FContacts) - 1 do
FContacts[I] := FContacts[I + 1];
SetLength(FContacts, Length(FContacts) - 1);
ApplyFilter(edtSearch.Text);
end;
end;
procedure TfrmAddressBook.sgContactsDblClick(Sender: TObject);
begin
btnEditClick(Sender); { double-click opens edit }
end;
end.
Key Design Observations
-
Separation of data and display: The
FContactsarray holds the data. The grid displays it. TheFFilteredIndicesarray maps visible rows to data indices, enabling search without modifying the underlying data. -
Real-time search: The
edtSearchChangehandler fires on every keystroke, callingApplyFilterwhich rebuilds the filtered indices and refreshes the grid. For small datasets this is instant; for large datasets you might add a debounce timer. -
Double-click to edit: The grid's
OnDblClickhandler simply callsbtnEditClick, reusing the same code path. This is a common usability pattern — users expect to double-click items to open them. -
Modal form reuse: The same
TfrmContactDetailform serves both Add and Edit operations. For Add, it opens empty. For Edit, it opens pre-populated. This avoids duplicating the form.
Extensions to Try
- Sort by column: Click a column header to sort the contacts by that column.
- Persistent storage: Save contacts to a file (CSV or JSON) and load them on startup.
- Import/Export: Add buttons to import contacts from a CSV file and export the current list.
- Contact groups: Add a TListBox sidebar showing groups (Family, Work, Friends) and filter the grid by group.
- Photo support: Add a TImage to the contact detail form for a contact photo.