r07 04 ojqz7ezhsgylnmtmxg4rpafsz7zr6cfrij52jhi OJQZ7EZHSGYLNMTMXG4RPAFSZ7ZR6CFRIJ52JHI


Rozdział 7. Bazodanowa
architektura Delphi
W niniejszym rozdziale zajmiemy się obsługą zewnętrznych baz danych przez aplikacje stworzone za pomocą
Delphi. Mimo iż do zrozumienia treści rozdziału konieczne jest pewne doświadczenie w pracy z bazami danych,
to jednak może on stanowić wprowadzenie do tematyki tworzenia aplikacji bazodanowych wysokiej jakości.
Również znawcy tematu znajdą tu ciekawe przykłady. Delphi 6 oferuje kilka mechanizmów dostępu do danych,
które przedstawimy tu w zarysie, zaś w następnych rozdziałach zajmiemy się niektórymi z nich bardziej
szczegółowo.
Typy baz danych
Wzorując się na systemie pomocy, możemy podzielić obsługiwane przez Delphi 6 technologie bazodanowe na
poniższe cztery grupy, z których każda związana jest z określoną stroną w palecie komponentów:
" BDE  zawiera komponenty wykorzystujące mechanizm Borland Database Engine (BDE). Mechanizm ten
oferuje szeroką gamę funkcji API organizujących współpracę z bazami danych. Znakomicie ułatwia obsługę
baz danych Paradox i dBase, jest jednak najbardziej skomplikowany pod względem procedury
instalacyjnej aplikacji końcowych.
" ADO  to grupa komponentów wykorzystujących ActiveX Data Objects (ADO), zapewniających dostęp do
baz danych za pośrednictwem mechanizmu OLEDB. Obiekty ADO stanowią standard Microsoftu; dostępna
jest bogata oferta sterowników zapewniających ich współpracę z rozmaitymi serwerami baz danych.
" dbExpress  technologia ta, posługując się efektywnymi sterownikami, zapewnia najszybszy dostęp do
informacji przechowywanych w bazach danych. Komponenty tej grupy mają charakter uniwersalny,
dostępne są także na platformie Linuksa. Uniwersalizm ten przesądza jednak o stosunkowo ubogim
repertuarze możliwości w zakresie manipulowania danymi.
" InterBase  komponenty tej grupy umożliwiają bezpośredni dostęp do serwera InterBase, z pominięciem
warstwy pośredniej (engine layer).
Architektura bazy danych
Z punktu widzenia aplikacji Delphi, przepływ informacji związanej z zarządzaniem bazami danych podzielić
można na ściśle zdefiniowane etapy. Każdemu z nich odpowiada określona grupa komponentów realizujących
specyficzne funkcje. Podział ten został schematycznie przedstawiony na rysunku 7.1.
Rysunek 7.1. Architektura bazy danych z punktu widzenia Delphi 6
Jak widać, interfejs użytkownika komunikuje się ze zbiorami danych poprzez pośrednią warstwę zródła danych
(datasource), reprezentowaną przez komponent TDataSource. Każdy z omawianych na wstępie typów baz
danych posługuje się innymi zbiorami danych, zaznaczonych w sposób symboliczny na rysunku 7.1;
abstrakcyjnym komponentem reprezentującym zbiór danych jest komponent TDataSet1.
Połączenia z serwerami baz danych
Ostatnim ogniwem schematu na rysunku 7.1 jest połączenie z danymi, zapewniające fizyczny dostęp do
informacji. Każdy z czterech wymienionych typów baz danych posługuje się innym komponentem
połączeniowym, wywodzącym się z klasy TCustomConnection:
" TDataBase jest komponentem połączeniowym dla zbiorów danych opartych na technologii BDE 
TTable, TQuery i TStoredProc. Jego wykorzystanie opisane zostało w rozdziale 28. książki  Delphi 4.
Vademecum profesjonalisty ( Aplikacje bazodanowe typu klient-serwer ).
" TADOConnection realizuje połączenie z bazami danych ADO, jak MS Access czy MS SQL.
Komponentami zbiorów danych ADO są TADODataSet, TADOTable, TADOQuery, i TADOStoredProc.
Technologią ADO zajmiemy się dokładniej w rozdziale 9.
" TSQLConnection to komponent połączeniowy dla zbiorów danych opartych na technologii dbExpress.
Są one efektywnymi, jednokierunkowymi (unidirectional) zbiorami danych reprezentowanymi przez
komponenty TSQLDataSet, TSQLTable, TSQLQuery i TSQLStoredProc. Technologii dbExpress
poświęcony jest rozdział 8. niniejszej książki.
" TIBDatabase jest komponentem połączeniowym dla zbiorów danych typu Interbase Express 
TIBDataSet, TIBTable, TIBQuery i TIBStoredProc. Zrezygnowaliśmy w niniejszej książce z opisu
komponentów Interbase Express, gdyż ich realizacja stanowi w dużej części naśladownictwo innych metod
połączeniowych.
Elementy wspólne dla wszystkich rodzajów połączeń składają się na definicję klasy TCustomConnection.
Zawiera ona metody, właściwości i zdarzenia związane z:
" nawiązywaniem i rozłączaniem połączenia z repozytoriami danych,
" logowaniem i nawiązywaniem bezpiecznego połączenia,
" zarządzaniem danymi.
Jest oczywiste, iż mimo owych wspólnych elementów funkcjonalnych, każdy z wymienionych komponentów
połączeniowych posiada pewne charakterystyczne cechy, wynikające ze specyfiki docelowego repozytorium
danych. Połączenie realizowane dla komponentów ADO różni się więc pod wieloma względami od połączenia
realizowanego dla komponentów BDE  o czym możesz się przekonać studiując rozdziały 8. i 9. niniejszej
książki oraz rozdział 28.  Delphi 4. Vademecum profesjonalisty .
1
Sens istnienia pośredniej warstwy zródła danych wyjaśniony został w rozdziale 16. książki  Delphi 6 dla każdego , wyd.
HELION 2001 (przyp. tłum.).
Zbiory danych
Zbiór danych (dataset) może być postrzegany jako dwuwymiarowa struktura kolumn (columns) i wierszy
(rows). Każda kolumna, zwana też polem ( field) grupuje w sobie dane tego samego rodzaju, natomiast kolejne
wiersze, zwane też rekordami (records), stanowią kolejne pozycje danych w zbiorze. Komponentem VCL,
ujmującym w sposób abstrakcyjny ideę zbioru danych jest komponent TDataSet, zawierający właściwości i
metody niezbędne do nawigowania wśród danych i manipulowania nimi, i stanowiący tym samym klasę bazową
dla komponentów realizujących konkretne zbiory danych związane z różnorodnymi technologiami bazodanowymi.
Aby uniknąć niejednoznaczności w dalszej części lektury, musimy zdefiniować kilka niezbędnych pojęć i
używać ich tylko w tym znaczeniu. Oto one:
" Zbiór danych (dataset) jest  zgodnie z tym, co powiedziano przed chwilą  uporządkowaną kolekcją
rekordów; organizację każdego rekordu, taką samą dla wszystkich rekordów, określają jego pola, z których
każde reprezentuje daną określonego typu (liczbę całkowitą, łańcuch znaków, liczbę w postaci znakowo-
dziesiętnej, grafikę itp.).
" Tabela (table) jest specjalnym typem zbioru danych, najczęściej posiadającym fizyczną postać pliku
dyskowego. Komponentami reprezentującymi różnorodne tabele są m.in. TTable, TADOTable,
TSQLTable i TIBTable.
" Zapytanie (query) również stanowi konkretyzację zbioru danych, jednak nie  zmaterializowaną w tak ścisły
sposób jak tabela, lecz stanowiącą swego rodzaju  tabelę tymczasową , która jest (zazwyczaj) wynikiem
żądania skierowanego pod adresem serwera. Zapytanie reprezentowane jest m.in. przez komponenty
TQuery, TADOQuery, TSQLQuery i TIBQuery.
Notatka
Jak wspominaliśmy na wstępie, niniejszy rozdział nie jest przeznaczony dla nowicjuszy i nie zawiera
obszernego wprowadzenia w problematykę programowania obsługi baz danych. Jeżeli określenia: baza
danych, tabela czy indeks brzmią dla Ciebie raczej obco, powinieneś uzupełnić swą wiedzę korzystając z
podręczników bardziej podstawowych2.
Otwieranie i zamykanie zbioru danych
Jakakolwiek operacja na zbiorze danych musi być poprzedzona jego otwarciem. Zadanie to wykonuje metoda
Open() klasy TDataSet:
Table1.Open;
Wywołanie to jest równoważne ustawieniu na True właściwości Active:
Table1.Active := True;
Czynnością wieńczącą wszystkie operacje na zbiorze danych jest jego zamknięcie, dokonywane przez
wywołanie metody Close()
Table1.Close;
2
jak np. książka wymieniona w poprzednim przypisie (przyp. tłum.).
równoważne ustawieniu właściwości Active na False:
Table1.Active := False;
Wskazówka
Jeżeli wykorzystujesz zbiory danych zlokalizowane na serwerze SQL, otwarcie pierwszego ze zbiorów tego
serwera poprzedzone jest nawiązaniem połączenia (connection) z serwerem, zaś zamknięcie ostatniego z
tych zbiorów powoduje rozłączenie się z serwerem (disconnection). Ponieważ operacje połączenia i rozłączenia
wymagają zawsze nieco czasu, korzystne może okazać się użycie komponentu TDatabase w celu
nawiązania permanentnego połączenia z serwerem w sytuacji, kiedy opisane przed chwilą połączenia
(rozłączenia) zdarzałyby się nazbyt często. Niebawem powrócimy do tego zagadnienia.
O tym, jak podobne jest otwieranie (zamykanie) różnych typów zbiorów danych, zaświadczyć może kod
prezentowany na wydruku 7.1.
Wydruk 7.1. Otwieranie i zamykanie przykładowych zbiorów danych
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, FMTBcd, DBXpress, IBDatabase, ADODB, DBTables, DB, SqlExpr,
IBCustomDataSet, IBQuery, IBTable, StdCtrls;
type
TForm1 = class(TForm)
SQLDataSet1: TSQLDataSet;
SQLTable1: TSQLTable;
SQLQuery1: TSQLQuery;
ADOTable1: TADOTable;
ADODataSet1: TADODataSet;
ADOQuery1: TADOQuery;
IBTable1: TIBTable;
IBQuery1: TIBQuery;
IBDataSet1: TIBDataSet;
Table1: TTable;
Query1: TQuery;
SQLConnection1: TSQLConnection;
Database1: TDatabase;
ADOConnection1: TADOConnection;
IBDatabase1: TIBDatabase;
Button1: TButton;
Label1: TLabel;
Button2: TButton;
IBTransaction1: TIBTransaction;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
procedure OpenDatasets;
procedure CloseDatasets;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
IBDatabase1.Connected := True;
ADOConnection1.Connected := True;
Database1.Connected := True;
SQLConnection1.Connected := True;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
OpenDatasets;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
CloseDatasets;
IBDatabase1.Connected := false;
ADOConnection1.Connected := false;
Database1.Connected := false;
SQLConnection1.Connected := false;
end;
procedure TForm1.CloseDatasets;
begin
// rozłączenie ze zbiorami dbExpress
SQLDataSet1.Close; // lub Active := false;
SQLTable1.Close; // lub Active := false;
SQLQuery1.Close; // lub Active := false;
// rozłączenie ze zbiorami ADO
ADOTable1.Close; // lub Active := false;
ADODataSet1.Close; // lub Active := false;
ADOQuery1.Close; // lub Active := false;
// rozłączenie ze zbiorami Interbase Express
IBTable1.Close; // lub Active := false;
IBQuery1.Close; // lub Active := false;
IBDataSet1.Close; // lub Active := false;
// rozłączenie ze zbiorami BDE
Table1.Close; // lub Active := false;
Query1.Close; // lub Active := false;
Label1.Caption := 'Zbiory danych są zamknięte.'
end;
procedure TForm1.OpenDatasets;
begin
// połączenie ze zbiorami dbExpress
SQLDataSet1.Open; // lub Active := true;
SQLTable1.Open; // lub Active := true;
SQLQuery1.Open; // lub Active := true;
// połączenie ze zbiorami ADO
ADOTable1.Open; // lub Active := true;
ADODataSet1.Open; // lub Active := true;
ADOQuery1.Open; // lub Active := true;
// połączenie ze zbiorami Interbase Express
IBTable1.Open; // lub Active := true;
IBQuery1.Open; // lub Active := true;
IBDataSet1.Open; // lub Active := true;
// połączenie ze zbiorami BDE
Table1.Open; // lub Active := true;
Query1.Open; // lub Active := true;
Label1.Caption := 'Zbiory danych są otwarte.';
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
CloseDatasets;
end;
end.
Powyższy moduł jest częścią projektu DatasetCnct.dpr znajdującego się na dołączonym do książki krążku
CD-ROM. Przed jego uruchomieniem należy odpowiednio ustawić ścieżki określające lokalizację baz danych
(ustawienia zawarte w projekcie pochodzą z oryginalnej wersji książki).
Nawigowanie wśród rekordów zbioru danych
Komponent TDataSet oferuje kilka podstawowych metod umożliwiających nawigowanie wśród rekordów
zbioru danych. Metody First()i Last()powodują przejście do (odpowiednio) pierwszego i ostatniego
rekordu, natomiast metody Next()i Prior()powodują przesunięcie się o jeden rekord (odpowiednio) w przód i
w tył. Przemieszczanie się o większą liczbę rekordów (w przód lub w tył) umożliwia metoda MoveBy() 
argument wywołania określa żądany dystans (wartość ujemna oznacza przesunięcie w tył).
Właściwości BOF i EOF i niebezpieczeństwo zapętlenia
Właściwości BOF i EOF (typu Boolean) umożliwiają wykrycie początku i końca tabeli. Oto przykładowa
sekwencja, powodująca wykonanie pewnej operacji kolejno na wszystkich rekordach zbioru danych:
Table1.First; // przejdz do pierwszego rekordu
While not Table1.EOF do // sprawdz, czy nie wystąpił koniec zbioru
begin
... // jakaś operacja na rekordzie
Table1.Next; // ta instrukcja jest bardzo ważna
end;
Ostrzeżenie:
Nie zapomnij o wywoływaniu metody Next() po przetworzeniu bieżącego rekordu, inaczej program wpadnie
w nieskończoną pętlę.
Pętli While nie można tu zastąpić pętlą Repeat  poniższa sekwencja nie będzie funkcjonować poprawnie
dla pustego zbioru danych, gdyż, jak wiadomo, pętla Repeat& Until wykonuje się co najmniej raz:
Table1.First;
Repeat
.... // Jakaś operacja na rekordzie
Table1.Next;
Until Table1.EOF;
Przykład nawigowania wśród rekordów różnych zbiorów danych ilustruje kolejny projekt (DatasetNav.dpr),
którego moduł główny przedstawiamy na wydruku 7.2.
Wydruk 7.2. Nawigowanie wśród rekordów zbioru danych
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, FMTBcd, DBXpress, IBDatabase, ADODB, DBTables, DB, SqlExpr,
IBCustomDataSet, IBQuery, IBTable, StdCtrls, Grids, DBGrids, ExtCtrls;
type
TForm1 = class(TForm)
SQLTable1: TSQLTable;
ADOTable1: TADOTable;
IBTable1: TIBTable;
Table1: TTable;
SQLConnection1: TSQLConnection;
Database1: TDatabase;
ADOConnection1: TADOConnection;
IBDatabase1: TIBDatabase;
Button1: TButton;
Label1: TLabel;
Button2: TButton;
IBTransaction1: TIBTransaction;
DBGrid1: TDBGrid;
DataSource1: TDataSource;
RadioGroup1: TRadioGroup;
btnFirst: TButton;
btnLast: TButton;
btnNext: TButton;
btnPrior: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Button2Click(Sender: TObject);
procedure RadioGroup1Click(Sender: TObject);
procedure btnFirstClick(Sender: TObject);
procedure btnLastClick(Sender: TObject);
procedure btnNextClick(Sender: TObject);
procedure btnPriorClick(Sender: TObject);
procedure DataSource1DataChange(Sender: TObject; Field: TField);
private
{ Private declarations }
procedure OpenDatasets;
procedure CloseDatasets;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
IBDatabase1.Connected := True;
ADOConnection1.Connected := True;
Database1.Connected := True;
SQLConnection1.Connected := True;
Datasource1.DataSet := IBTable1;
OpenDatasets;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
OpenDatasets;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
CloseDatasets;
IBDatabase1.Connected := false;
ADOConnection1.Connected := false;
Database1.Connected := false;
SQLConnection1.Connected := false;
end;
procedure TForm1.CloseDatasets;
begin
// rozłączenie ze zbiorem dbExpress
SQLTable1.Close; // lub Active := false;
// rozłączenie ze zbiorem ADO
ADOTable1.Close; // lub Active := false;
// rozłączenie ze zbiorem Interbase Express
IBTable1.Close; // lub Active := false;
// rozłączenie ze zbiorem BDE
Table1.Close; // lub Active := false;
Label1.Caption := 'Zbiory danych są zamknięte.'
end;
procedure TForm1.OpenDatasets;
begin
// połączenie ze zbiorem dbExpress
SQLTable1.Open; // lub Active := true;
// połączenie ze zbiorem ADO
ADOTable1.Open; // lub Active := true;
// połączenie ze zbiorem Interbase Express
IBTable1.Open; // lub Active := true;
// połączenie ze zbiorem BDE
Table1.Open; // lub Active := true;
Label1.Caption := 'Zbiory danych są otwarte.';
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
CloseDatasets;
end;
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
case RadioGroup1.ItemIndex of
0: Datasource1.DataSet := IBTable1;
1: Datasource1.DataSet := Table1;
2: Datasource1.DataSet := ADOTable1;
end; // case
end;
procedure TForm1.btnFirstClick(Sender: TObject);
begin
DataSource1.DataSet.First;
end;
procedure TForm1.btnLastClick(Sender: TObject);
begin
DataSource1.DataSet.Last;
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
DataSource1.DataSet.Next;
end;
procedure TForm1.btnPriorClick(Sender: TObject);
begin
DataSource1.DataSet.Prior;
end;
procedure TForm1.DataSource1DataChange(Sender: TObject; Field: TField);
begin
btnLast.Enabled := not DataSource1.DataSet.Eof;
btnNext.Enabled := not DataSource1.DataSet.Eof;
btnFirst.Enabled := not DataSource1.DataSet.Bof;
btnPrior.Enabled := not DataSource1.DataSet.Bof;
end;
end.
Formularz projektu przedstawia rysunek 7.2. Za pomocą przycisków z grupy  Zbiór danych użytkownik może
wybrać jeden z trzech rodzajów zbiorów danych. Obecne na formularzu przyciski służą do otwierania i
zamykania wybranego zbioru oraz poruszania się po jego rekordach.
Rysunek 7.2. Formularz główny projektu DatasetNav.dpr
Notatka
Jak zapewne zauważyłeś, nie włączyliśmy do powyższego projektu zbiorów danych typu dbExpress. Nie
zrobiliśmy tego z prostej przyczyny  są one zbiorami jednokierunkowymi (unidirectional), przeznaczonymi
tylko do odczytu; próba dołączenia do takiego zbioru  nawigowalnego komponentu w rodzaju TDBGrid
spowoduje wyjątek. Nawigacja w zbiorach jednokierunkowych musi być prowadzona za pomocą specjalnych
środków  zajmiemy się tym w rozdziale 8.
Manipulowanie zawartością zbioru danych
Dostęp do zbiorów danych niewiele byłby wart, gdyby nie możliwość manipulowania jego danymi 
rozumianego jako wstawianie, edycja i usuwanie rekordów. Komponent TDataSet posiada związane z tym
metody Insert(), Edit() i Delete(), zaś przykład ich użycia przedstawia wydruk 7.3, stanowiący część
projektu DataManip.dpr.
Wydruk 7.3. Manipulowanie zawartością zbioru danych
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Mask, DBCtrls, DB, Grids, DBGrids, ADODB;
type
TMainForm = class(TForm)
ADOConnection1: TADOConnection;
adodsCustomer: TADODataSet;
dtsrcCustomer: TDataSource;
DBGrid1: TDBGrid;
adodsCustomerCustNo: TAutoIncField;
adodsCustomerCompany: TWideStringField;
adodsCustomerAddress1: TWideStringField;
adodsCustomerAddress2: TWideStringField;
adodsCustomerCity: TWideStringField;
adodsCustomerStateAbbr: TWideStringField;
adodsCustomerZip: TWideStringField;
adodsCustomerCountry: TWideStringField;
adodsCustomerPhone: TWideStringField;
adodsCustomerFax: TWideStringField;
adodsCustomerContact: TWideStringField;
Label1: TLabel;
dbedtCompany: TDBEdit;
Label2: TLabel;
dbedtAddress1: TDBEdit;
Label3: TLabel;
dbedtAddress2: TDBEdit;
Label4: TLabel;
dbedtCity: TDBEdit;
Label5: TLabel;
dbedtState: TDBEdit;
Label6: TLabel;
dbedtZip: TDBEdit;
Label7: TLabel;
dbedtPhone: TDBEdit;
Label8: TLabel;
dbedtFax: TDBEdit;
Label9: TLabel;
dbedtContact: TDBEdit;
btnAdd: TButton;
btnEdit: TButton;
btnSave: TButton;
btnCancel: TButton;
Label10: TLabel;
dbedtCountry: TDBEdit;
btnDelete: TButton;
procedure btnAddClick(Sender: TObject);
procedure btnEditClick(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
procedure btnCancelClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure btnDeleteClick(Sender: TObject);
private
{ Private declarations }
procedure SetButtons;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure TMainForm.btnAddClick(Sender: TObject);
begin
adodsCustomer.Insert;
SetButtons;
end;
procedure TMainForm.btnEditClick(Sender: TObject);
begin
adodsCustomer.Edit;
SetButtons;
end;
procedure TMainForm.btnSaveClick(Sender: TObject);
begin
adodsCustomer.Post;
SetButtons;
end;
procedure TMainForm.btnCancelClick(Sender: TObject);
begin
adodsCustomer.Cancel;
SetButtons;
end;
procedure TMainForm.SetButtons;
begin
btnAdd.Enabled := adodsCustomer.State = dsBrowse;
btnEdit.Enabled := adodsCustomer.State = dsBrowse;
btnSave.Enabled := (adodsCustomer.State = dsInsert) or
(adodsCustomer.State = dsEdit);
btnCancel.Enabled := (adodsCustomer.State = dsInsert) or
(adodsCustomer.State = dsEdit);
btnDelete.Enabled := adodsCustomer.State = dsBrowse;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
adodsCustomer.Open;
SetButtons;
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
adodsCustomer.Close;
ADOConnection1.Connected := False;
end;
procedure TMainForm.btnDeleteClick(Sender: TObject);
begin
adodsCustomer.Delete;
end;
end.
Formularz projektu przedstawia rysunek 7.3. Projekt realizuje manipulowanie danymi w najprostszej postaci.
Analizując kod zródłowy formularza głównego bez trudu zauważysz wykorzystanie następujących metod klasy
TDataSet:
" Insert()  przygotowuje zbiór danych do wstawienia nowego rekordu;
" Edit()  przygotowuje zbiór danych do edycji bieżącego rekordu;
" Post()  zatwierdza zmiany poczynione w rekordzie (edytowanym lub wstawianym);
" Cancel()  anuluje zmiany wprowadzone do rekordu;
" Delete()  usuwa bieżący rekord ze zbioru.
Rysunek 7.3. Formularz główny projektu DataManip.dpr
Wydruk 7.3 ilustruje ponadto zastosowanie właściwości TDataset.State, reprezentującej stan zbioru; jest ona
wykorzystywana do selektywnego udostępniania poszczególnych przycisków  i tak, np. przycisk Dodaj jest
niedostępny, gdy zbiór danych znajduje się w stanie wstawiania (dsInsert) bądz edytowania (dsEdit)
rekordu. Znaczenie poszczególnych wartości, które przyjmować może właściwość State, wyjaśnione zostało w
tabeli 7.1.
Tabela 7.1. Wartości właściwości TDataSet.State
Wartość Znaczenie
dsBrowse
Stan przeglądania.
dsCalcFields
Realizowana jest procedura obsługi zdarzenia
OnCalcFields  trwa ustalanie wartości pól
obliczanych.
dsEdit
Stan edycji  wywołano metodę Edit(), lecz zmiany nie
zostały jeszcze utrwalone.
dsInactive
Zbiór danych jest zamknięty.
dsInsert
Stan wstawiania lub dołączania nowego rekordu 
wywołano metodę Insert() lub Append(), lecz zmiany
nie zostały jeszcze utrwalone.
dsSetKey
Wywołano metodę SetKey(), lecz nie wywołano jeszcze
metody GotoKey().
dsNewValue
Zbiór danych znajduje się w tymczasowym stanie, w
którym odczytywana jest właściwość NewValue.
dsUpdateOld
Zbiór danych znajduje się w tymczasowym stanie, w
którym odczytywana jest właściwość OldValue.
dsFilter
Trwa wykonywanie jakiejś operacji związanej z filtrowaniem.
dsBlockRead
Z powodu buforowania danych, przy zmianie bieżącego
rekordu zawartość kontrolek bazodanowych nie zostanie
uaktualniona, nie będą też generowane zdarzenia
odpowiadające zmianie pozycji.
dsInternalCalc
Zbiór danych znajduje się w tymczasowym stanie związanym
z obliczaniem pola, którego właściwość FieldKind równa
jest fkInternalCalc.
dsOpening
Trwa otwieranie zbioru danych; stan ten pojawia się podczas
otwierania do odczytu asynchronicznego.
Pola rekordu bazy danych
Dostęp do poszczególnych pól rekordu w zbiorze danych umożliwia komponent TField i jego komponenty
pochodne. Oprócz odczytywania i zapisywania pól możliwa jest zmiana ich właściwości, jak również tworzenie
nowych pól obliczanych (calculated fields) i przeglądowych (lookup fields).
Wartości pól
Bezpośrednim narzędziem służącym do odczytu i modyfikacji pól rekordu jest tablicowa właściwość
FieldValues[] komponentu TDataSet  każdy element tej tablicy ma typ Variant. Ponieważ właściwość
FieldValues[] jest domyślną właściwością tablicową klasy TDataSet, można opuścić jej nazwę i stosować
operator indeksowania wprost do zmiennej obiektowej, jak w poniższym przykładzie:
S := Table1['CustName'];
// to samo, co S := Table1.FieldValues['CustName']
Jak przed chwilą wspomnieliśmy, wartości otrzymywane za pośrednictwem właściwości FieldValues[] są
typu Variant, co stanowi niejako dodatkową atrakcję  możliwe jest między innymi zapamiętanie zawartości
wszystkich pól rekordu w pojedynczej tablicy wariantowej, co ilustruje poniższy przykład:
const
Astr = 'Pracownik %s pracuje na stanowisku nr %s i posiada
stawkę podstawową %m' ;
var
VarArr: Variant
F: Currency;
begin
VarArr := VarArrayCreate([0, 2], varVariant);
VarArr := Table1['Nazwisko;Stanowisko;Stawka'];
F := VarArr[2];
ShowMessage(Format(Astr, [VarArr[0], VarArr[1], F]));
end;
Wartości pól rekordu dostępne są także (pośrednio) za pośrednictwem właściwości Fields[] i metody
FieldsByName(). Pierwsza z nich jest indeksowaną od zera tablicą komponentów TField reprezentujących
poszczególne pola  Fields[0] oznacza pierwsze pole w rekordzie; indeksem może być również nazwa pola.
Metoda FieldsByName() udostępnia komponent TField skojarzony z polem o danej nazwie, stanowiącej
parametr jej wywołania  na przykład:
Nazwisko := Table1.FieldByName('Nazwisko');
Mając już konkretny obiekt TField, możemy odczytać lub zapisać wartość reprezentowanego przez niego pola 
służy do tego szereg właściwości, związanych z różnymi typami wartości pól. Przedstawia je tabela 7.2.
Tabela 7.2. Właściwości komponentu TField udostępniające wartość pola
Właściwość Typ wartości pola
AsBoolean Boolean
AsFloat Double
AsInteger Longint
AsString String
AsDateTime TDateTime
Value Variant
I tak, na przykład, jeżeli pierwsze pole zbioru danych ma nazwę Nazwisko i wartość typu String, jego wartość
można otrzymać za pomocą jednej z poniższych instrukcji
S := Table1.Fields[0].AsString;
S := Table1.FieldsByName('Nazwisko').AsString;
Typy pól
Informacja o typie pola zawarta jest we właściwości DataType odnośnego komponentu TField. Właściwość ta
przyjmować może wartości następującego typu:
type
TFieldType = (ftUnknown, ftString, ftSmallint, ftInteger, ftWord, ftBoolean, ftFloat,
ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob,
ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary, ftCursor,
ftFixedChar, ftWideString, ftLargeint, ftADT, ftArray, ftReference, ftDataSet,
ftOraBlob, ftOraClob, ftVariant, ftInterface, ftIDispatch, ftGuid, ftTimeStamp,
ftFMTBcd);
Różnorodne typy pól znajdują w Delphi jeszcze inne odzwierciedlenie  w postaci różnorodnych klas
pochodnych w stosunku do TField; niebawem powrócimy do tej kwestii.
Nazwy i numery pól
Nazwa pola kryje się pod właściwością FieldName reprezentującego je komponentu TField, tak więc nazwę
pola o numerze N (licząc od zera) uzyskać można za pomocą instrukcji
S := Table1.Fields[N].FieldName;
Funkcję odwrotną  uzyskanie numeru pola na podstawie jego nazwy  realizuje właściwość FieldNo:
N := Table1.FieldByName('Adres').FieldNo;
Liczbę pól w rekordzie zbioru danych udostępnia (pośrednio) właściwość TDataSet.FieldList,
reprezentująca  spłaszczoną (flattened) listę pól, wraz z polami zagnieżdżonymi i abstrakcyjnymi polami
danych (ADT). Dla kompatybilności zachowano również właściwość TDataSet.FieldCount, nie uwzględnia
ona jednak pól ADT.
Operowanie zawartością pól
Proces edycji jednego lub kilku pól rekordu realizowany jest w trzech następujących etapach:
1. Wywołanie metody Edit(), przełączającej zbiór danych w tryb edycji.
2. Przypisanie nowych wartości wybranym polom rekordu.
3. Utrwalenie wprowadzonych zmian, bądz w sposób jawny przez wywołanie metody Post(), bądz w sposób
automatyczny, przez zmianę bieżącej pozycji w zbiorze.
Oto prosty przykład:
Table1.Edit;
Table1['Wiek'] := 44;
Table1.Post;
Wskazówka
Niektóre zbiory danych są przewidziane tylko do odczytu  na przykład zbiory zapisane na krążkach
CD-ROM, czy też wirtualne zbiory danych będące wynikami zapytań SQL. W takim wypadku modyfikacja pól
jest oczywiście niewykonalna; o tym, czy pola zbioru danych można modyfikować, informuje jego właściwość
CanModify  wartość True oznacza zezwolenie na modyfikację.
W równie prosty sposób można dodać do zbioru danych nowy rekord. W tym celu należy:
1. Przełączyć zbiór danych w tryb wstawiania lub dołączania przez wywołanie jednej z metod Insert()albo
Append().
2. Przypisać wartości wybranym polom nowego rekordu.
3. Utrwalić zmianę za pomocą wywołania metody Post() lub przemieszczenia się do innego rekordu.
Ostrzeżenie:
Przy edycji, wstawianiu lub dołączaniu rekordu do zbioru bądz świadom tego, że każda zmiana bieżącego
rekordu powoduje utrwalenie wprowadzonych zmian, istniejących dotychczas tylko na  formatce edycyjnej.
Używaj więc metod First(), Next(), Prior() i Last() i MoveBy() ze szczególną ostrożnością.
Wycofanie się z wprowadzonych zmian możliwe jest dzięki metodzie Cancel(). Anuluje ona wszystkie zmiany
i przełącza bazę w tryb przeglądania. Oto prosty przykład:
Table1.Edit;
Table1['Wiek'] := 44;
if Potwierdzenie
Then
Table1.Post
Else
Table1.Cancel;
I wreszcie ostatnia metoda związana z operowaniem danymi  jest nią metoda Delete(), dokonująca
usunięcia bieżącego rekordu zbioru. Oto przykład  skrócenia zbioru o jeden rekord:
Table1.Last;
Table1.Delete;
Edytor pól
Edytor pól (rysunek 7.4) jest narzędziem umożliwiającym operowanie zestawem pól oraz ich atrybutami. Można
go uruchomić w dwojaki sposób: przez dwukrotne kliknięcie komponentu reprezentującego zbiór danych (np.
TTable, TQuery, TStoredProc), bądz też przez wybranie pozycji Fields Editor z jego menu
kontekstowego (uruchamianego kliknięciem prawym przyciskiem myszy).
Rysunek 7.4. Edytor pól i jego menu kontekstowe
Edytor pól umożliwia wybranie tych pól (spośród faktycznie istniejących w zbiorze danych), które widoczne
będą dla reprezentującego zbiór danych komponentu. Domyślnie widoczne są wszystkie pola  okno edytora
pól nie zawiera wówczas żadnej pozycji. Edytor pól umożliwia ponadto definiowanie własnych pól
przeglądowych i obliczanych  służą do tego polecenia menu kontekstowego (widocznego na rysunku 7.4).
Aby samodzielnie poeksperymentować z edytorem pól, zainicjuj nową aplikację, umieść na formularzu
komponent TTable i ustaw odpowiednio jego komponenty: TableName na orders.db i DatabaseName na
DBDEMOS (ta ostatnia nazwa jest aliasem katalogu zawierającego przykładowe bazy danych Delphi 6). Aby
uzyskać wgląd w strukturę i zawartość tabeli, umieść także na formularzu komponenty TDataSource i
TDBGrid, ustawiając odpowiednio ich właściwości  DBGrid1.DataSource na DataSource1 oraz
DataSource1.DataSet na Table1. Po ustawieniu na True właściwości Table1.Active w przeglądarce
DBGrid1 ukaże się zawartość rekordów zbioru danych.
Dodawanie pól
Okno edytora pól pozostaje domyślnie puste  jest tak podczas jego pierwszego uruchomienia dla nowo
dołączonego zbioru danych; oznacza to, iż widoczne są wszystkie pola, co łatwo stwierdzić zerknąwszy na
przeglądarkę. Sytuacja ta zmieni się jednak, gdy dodamy któreś pole do listy widocznej w oknie edytora 
wówczas to owa lista określać będzie zestaw pól widocznych.
Dodanie nowego pola do wspomnianej listy następuje w wyniku wybrania opcji Add Fields z menu
kontekstowego widocznego na rysunku 7.4; ukaże się wtedy okno zawierające listę wszystkich pól tabeli.
Wybierz z niej kilka pól  na przykład OrderNo, CustNo i ItemsTotal i kliknij przycisk OK; wybrane pola
(i tylko one) będą odtąd widoczne w przeglądarce i w oknie edytora pól.
Komponenty reprezentujące pola
Dla każdego pola wybranego przez edytor Delphi tworzy komponent będący pochodnym w stosunku do
TField, o typie zależnym od typu pola; właścicielem tak utworzonego komponentu jest formularz. Dla
wymienionych przed chwilą pól OrderNo, CustNo i ItemsTotal utworzone zostaną następujące komponenty:
Table1OrderNo: TFloatField;
Table1CustNo: TFloatField;
Table1ItemsTotal: TCurrencyField;
Jak łatwo zauważyć, nazwa każdego z powyższych pól formularza jest konkatenacją nazwy komponentu
reprezentującego zbiór danych i nazwy pola w tym zbiorze.
Zestawienie (większości) komponentów pochodnych do TField, wraz z ich klasami macierzystymi i
pascalowymi odpowiednikami typów, przedstawia tabela 7.3.
Tabela 7.3. Typy pól baz danych w Delphi
Komponent Komponent macierzysty Typ pola Typ Object Pascala
TStringField TField ftString String
TWideStringField TStringField ftWideString WideString
TGuidField TStringField ftGuid TGUID
TNumericField TField * *
TIntegerField TNumericField ftInteger Integer
TSmallIntField TIntegerField ftSmallint SmallInt
TLargeintField TNumericField ftLargeint Int64
TWordField TIntegerField ftWord Word
TAutoIncField TIntegerField ftAutoInc Integer
TFloatField TNumericField ftFloat Double
TCurrencyField TFloatField ftCurrency Currency
TBCDField TNumericField ftBCD Double
TBooleanField TField ftBoolean Boolean
TDateTimeField TField ftDateTime TDateTime
TDateField TDateTimeField ftDate TDateTime
TTimeField TDateTimeField ftTime TDateTime
TBinaryField TField * *
TBytesField TBinaryField ftBytes nie istnieje
TVarBytesField TBytesField ftVarBytes nie istnieje
TBlobField TField ftBlob nie istnieje
TMemoField TBlobField ftMemo nie istnieje
TGraphicField TBlobField ftGraphic nie istnieje
TObjectField TField * *
TADTField TObjectField ftADT nie istnieje
TArrayField TObjectField ftArray nie istnieje
TDataSetField TObjectField ftDataSet TDataSet
TReferenceField TDataSetField ftReference nie istnieje
TVariantField TField ftVariant OleVariant
TInterfaceField TField ftInterface IUnknown
TIDispatchField TInterfaceField ftIDispatch IDispatch
TAggregateField TField nie istnieje nie istnieje
* oznacza abstrakcyjną klasę bazową
Pola a inspektor obiektów
Wybierając pojedyncze pole w oknie edytora pól, użytkownik uzyskuje dostęp do jego właściwości z poziomu
inspektora obiektów. Umożliwia to ustalanie (na etapie projektowania) wielu cech pola, jak np. minimalna i
maksymalna wartość, format wyświetlania, dostępność do modyfikacji itp.
Przejście na stronę Events inspektora obiektów umożliwia z kolei przypisywanie procedur obsługi do
poszczególnych zdarzeń pola, jak OnChange, OnGetTExt, OnSetText, OnValidate itp.  ich znaczenie
opisane jest w systemie pomocy, wystarczy kliknąć nazwę zdarzenia i nacisnąć F1. Bodaj najczęściej
wykorzystywanym zdarzeniem pola jest OnChange, zachodzące każdorazowo, gdy ulega zmianie zawartość pola,
a więc np. podczas edycji rekordu lub zmiany bieżącej pozycji w zbiorze danych.
Pola obliczane
Za pomocą edytora pól możliwe jest utworzenie tzw. pola obliczanego (calculated field), które jest zaliczane do
jednego z rodzajów pól wirtualnych, tj. nie istniejących fizycznie w zbiorze danych. Załóżmy na przykład, że
potrzebne jest nam pole o wartości zmniejszonej o 32% w stosunku do pola ItemsTotal. Jego zdefiniowanie
wymaga wykonania dwóch czynności: utworzenia odpowiedniego komponentu pochodnego do TField oraz
wskazania sposobu obliczania zawartości.
Pierwszą z tych czynności wykonuje się bardzo łatwo za pomocą edytora pól. Po zamknięciu zbioru danych
(Table1.Active ustawione na False) wystarczy wybrać z menu kontekstowego polecenie New Field, aby
otrzymać okno przedstawione na rysunku 7.5. Następnie w pole Name należy wpisać nazwę nowego pola
(WholesaleTotal), w polu Type należy wybrać jego typ (Currency) i zaznaczyć opcję Calculated. Po
kliknięciu przycisku OK i ponownym otwarciu zbioru, w przeglądarce DBGrid pojawi się nowa, pusta kolumna.
Rysunek 7.5. Definiowanie pola obliczanego
Aby zapełnić tę kolumnę poprawnymi danymi, należy zrealizować drugi etap scenariusza  zdefiniować sposób
obliczania pola. Ustalanie wartości wszystkich pól obliczanych danego rekordu odbywa się w ramach zdarzenia
OnCalcFields zbioru danych. Za pomocą inspektora obiektów należy więc utworzyć szkielet procedury
obsługującej to zdarzenie i wpisać do jej wnętrza odpowiednią instrukcję:
procedure TForm1.Table1CalcFields(DataSet: TDataSet);
begin
DataSet['WholeSalesTotal'] := DataSet['ItemsTotal'] * 0.68;
end;
Po skompilowaniu i uruchomieniu projektu ujrzymy w przeglądarce wyliczoną wartość pola w każdym
rekordzie (rys. 7.6).
Rysunek 7.6. Pole obliczane widoczne w przeglądarce
Pola przeglądowe
Inną odmianą pola wirtualnego jest pole przeglądowe (lookup field). Pole takie czerpie swą wartość z innego,
skorelowanego zbioru danych. Wyjaśnimy to na prostym przykładzie.
Załóżmy, że w zbiorze danych ORDERS.DB, zawierającym rejestr zamówień, klient zamawiający towar
identyfikowany jest przez numer umieszczony w polu CustNo. Trzeba jednak przyznać, że list zaadresowany
jako  CN 1384 miałby nikłe szanse trafić do właściwego adresata (chyba że zaprzyjazniony listonosz również
zaopatruje się w tej firmie); pożądane byłoby więc pole, zawierające nazwę lub nazwisko klienta, czytelne nie
tylko dla komputera, lecz także dla użytkownika.
Aby to wykonać, musimy dysponować innym zbiorem danych, zawierającym rekordy opisujące poszczególnych
klientów, identyfikowanych za pomocą tych samych numerów, które używane są w zbiorze orders.db. Taki
zbiór oczywiście istnieje, jest nim customer.db, umieszczony w tym samym katalogu, identyfikowanym przez
alias DBDEMOS.
Umieśćmy więc na formularzu drugi element TTable (otrzyma on nazwę Table2) i skojarzmy go ze
wspomnianym zbiorem customer.db. Wzajemne przyporządkowanie rekordów pomiędzy zbiorami
orders.db oraz customer.db następuje w ramach tzw. dialogu połączeniowego, uruchamianego za
pośrednictwem edytora pól.
Otwieramy edytor pól (dla komponentu Table1), z jego menu kontekstowego wybieramy polecenie New Field
i nadajemy nowemu polu nazwę CustName, typ String i szerokość 15 znaków. W sekcji Field type
wybieramy opcję Lookup.
Kryterium odpowiedniości rekordów w obydwu zbiorach jest równość wskazanych pól; w zbiorze orders.db
jest to pole CustNo, w zbiorze customer.db  pole CustNo (nazwy nie muszą być identyczne). Wskazujemy
te pola jako (odpowiednio) Key Fields i Lookup Keys. Należy jeszcze tylko wskazać komponent
reprezentujący pomocniczy zbiór danych (Table2) i pole w tym zbiorze, zawierające wartość dla definiowanego
pola przeglądowego (Contact). Wypełnione okienko edytora połączeń przedstawia rysunek 7.7.
Rysunek 7.7. Definiowanie pola przeglądowego
Po otwarciu obydwu zbiorów (Table1 i Table2) ujrzymy zawartość nowego pola CustName (rys. 7.8):
Rysunek 7.8. Pole przeglądowe CustName w przeglądarce
Przeciąganie pól
Jak już wspominaliśmy, dla każdego uwzględnianego pola edytor pól automatycznie tworzy komponent
pochodny do TField. To jednak nie koniec udogodnień: mniej popularnym, lecz niezwykle poręcznym
mechanizmem jest coś, co wizualnie można określić jako przeciąganie (drag-and-drop) pól z okna edytora pól
wprost na formularz.
Aby to zaobserwować, umieść na (pustym) formularzu komponent TTable i skojarz go ze zbiorem
biolife.db (w bazie DBDEMOS); umieść także komponent TDataSource i skojarz go z komponentem
TTable. W edytorze pól wybierz wszystkie pola (Add all fields), a następnie przeciągnij niektóre z nich
(lub wszystkie) na formularz. Po uporządkowaniu komponentów formularz powinien wyglądać mniej więcej tak,
jak na rysunku 7.9.
Rysunek 7.9. Rezultat przeciągnięcia pól z edytora pól na formularz
Przeanalizujmy, co się właściwie stało. Po pierwsze, dla każdego przeciągniętego pola Delphi wybrało najlepiej
pasującą do jego typu kontrolkę prezentacyjną: dla pól łańcuchowych jest to TDBEdit, dla grafiki  TDBImage
itp.); każda z kontrolek opatrzona została ponadto etykietą zawierającą nazwę pola. Po drugie  aby
wspomniane kontrolki mogły uzyskać łączność ze zbiorem danych, potrzebny jest powiązany z nim komponent
TDataSource; Delphi sprawdziło, iż takowy istnieje już na formularzu (w przeciwnym razie utworzyłoby ad
hoc nowy). Po otwarciu zbioru kontrolki prezentacyjne wypełniły się zawartością odpowiadających im pól w
bieżącym (pierwszym) rekordzie zbioru.
Pola typu BLOB
Pola tego typu (Binary Large OBject   duży obiekt binarny ) przeznaczone są do przechowywania danych
binarnych bez określonej struktury czy wielkości  to samo pole BLOB może w jednym rekordzie mieć
wielkość 3 bajtów, w innym natomiast  3 kilobajtów. Pola BLOB są szczególnie przydatne do
przechowywania dużych porcji tekstu, grafiki oraz  surowych strumieni danych, których doskonałym
przykładem są obiekty OLE.
TBlobField a inne typy pól
Zgodnie z tabelą 7.3, komponent TBlobField wywodzi się z klasy TField. Zbiór typów pól, które mogą być
przez niego reprezentowane jest podzbiorem typu TFieldType i ma następującą definicję:
TBlobType = ftBlob .. ftOraClob;
zaś typ aktualnie reprezentowanego pola określony jest przez właściwość BlobType. Znaczenie poszczególnych
jej wartości zostało wyjaśnione w tabeli 7.4.
Tabela 7.4. Typy pól BLOB
Typ pola Rodzaj danych zawartych w polu
ftBlob
Dane nieskategoryzowane lub w formacie definiowanym
przez użytkownika
ftMemo
Informacja tekstowa
ftGraphic
Bitmapa Windows
ftFmtMemo
Sformatowane pole Memo systemu Paradox
ftParadoxOLE
Obiekt OLE Paradoxa
ftDBaseOLE
Obiekt OLE dBase a
ftTypedBinary
Binarna reprezentacja zdefiniowanego typu danych
ftCursor..ftDataSet
Niedopuszczalne dla pola BLOB
ftOraBlob
Pola BLOB tabeli Oracle 8
ftOraClob
Pola CLOB tabeli Oracle 8
W większości przypadków  obróbka pól typu BLOB sprowadza się do ich przechowywania w strumieniu; ich
zapisywanie i odczytywanie ułatwia specjalny rodzaj strumienia  TBlobStream, fizycznie stanowiący
strumień zlokalizowany wewnątrz tabeli. Poniższy przykład z pewnością pomoże zrozumieć jego
funkcjonowanie.
Przykładowy projekt wykorzystujący pola typu BLOB
Projekt Wavez.dpr znajduje się na załączonym krążku CD-ROM; wykorzystano w nim niektóre mechanizmy
Windows, nie jest więc zgodny ze standardami CLX. Jego formularz główny przedstawia rysunek 7.10.
Komponent TTable powiązany jest z tabelą o następującej strukturze:
Nazwa pola Typ pola Rozmiar pola
WaveTitle Character 25
FileName Character 25
Wave BLOB
Rysunek 7.10. Formularz główny projektu Wavez.dpr
Przycisk oznaczony plusem służy do wczytania materiału dzwiękowego z pliku dyskowego i dodania go do
tabeli  wykonuje to nieskomplikowana procedura obsługująca zdarzenie kliknięcia przycisku:
procedure TMainForm.sbAddClick(Sender: TObject);
begin
if OpenDialog.Execute then
begin
tblSounds.Append;
tblSounds['FileName'] := ExtractFileName(OpenDialog.FileName);
tblSoundsWave.LoadFromFile(OpenDialog.FileName);
edTitle.SetFocus;
end;
end;
Po wybraniu konkretnego pliku (w ramach dialogu OpenDialog) następuje przełączenie zbioru danych w tryb
dołączania rekordu (Append). Następnie funkcja ExtractFileName()  oczyszcza specyfikację pliku z
ewentualnej ścieżki, po czym nazwa pliku wpisywana jest do pola FileName zbioru danych tblSounds.
Kolejna instrukcja dokonuje wczytania materiału dzwiękowego do pola BLOB reprezentowanego przez
komponent tblSoundsWave  zwróć uwagę, iż cała ta operacja sprowadza się do wywołania jednej metody
(LoadFromFile). Wczytany materiał nie posiada jeszcze tytułu (pole WaveTitle nowego rekordu pozostaje
niewypełnione), dlatego też po zakończeniu wczytywania aktywnym komponentem staje się edTitle, do
którego użytkownik może wpisać wybrany przez siebie tytuł.
Równie nieskomplikowany jest zapis materiału dzwiękowego do zewnętrznego pliku, następujący w wyniku
kliknięcia przycisku oznaczonego ikoną dyskietki:
procedure TMainForm.sbSaveClick(Sender: TObject);
begin
with SaveDialog do
begin
FileName := tblSounds['FileName']; // nadaj nazwę pliku
if Execute then // dialog
tblSoundsWave.SaveToFile(FileName); // zapisz BLOB w pliku
end;
end;
W oknie dialogowym (SaveDialog) służącym do wyboru pliku dyskowego, domyślna nazwa pliku pobierana
jest z pola FileName bieżącego rekordu zbioru danych tblSounds; zapis zawartości pola BLOB w pliku
realizowany jest za pomocą pojedynczej metody (SaveToFile) komponentu reprezentującego to pole
(tblSoundsWave).
Odtworzenie materiału dzwiękowego zawartego w polu BLOB jest już jednak bardziej złożone. Samo
odtwarzanie realizowane jest przez funkcję API o nazwie PlaySound(), konieczne jest jednak wykonanie kilku
dodatkowych czynności pomocniczych:
procedure TMainForm.sbPlayClick(Sender: TObject);
var
B: TBlobStream;
M: TMemoryStream;
begin
B := TBlobStream.Create(tblSoundsWave, bmRead); // utwórz strumień BLOB
Screen.Cursor := crHourGlass; // kursor sygnalizujący
// oczekiwanie
try
M := TMemoryStream.Create; // utwórz strumień pamięciowy
try
M.CopyFrom(B, B.Size); // kopiuj ze strumienia BLOB
// do pamięciowego
// Spróbuj odtworzyć plik dzwiękowy; wygeneruj wyjątek,
// gdy coś się nie powiedzie
Win32Check(PlaySound(M.Memory, 0, SND_SYNC or SND_MEMORY));
finally
M.Free;
end;
finally
Screen.Cursor := crDefault;
B.Free; // zwolnij strumień
end;
end;
Powyższa procedura rozpoczyna swą pracę od utworzenia strumienia TBlobStream na bazie pola BLOB
przechowującego materiał dzwiękowy. Pierwszy argument wywołania konstruktora jest nazwą odnośnego pola,
drugi natomiast określa zamierzony sposób jego używania  bmRead oznacza wyłącznie odczyt, bmReadWrite
także zapis.
Wskazówka
Użycie trybu bmReadWrite w konstruktorze strumienia TBlobStream wymaga, aby zbiór danych
zawierający odnośne pole BLOB znajdował się w stanie edycji, wstawiania lub dołączania rekordu.
Sam strumień TBlobStream nie daje się jednak odtworzyć w sposób bezpośredni  materiał dzwiękowy dla
funkcji PlaySound() musi znajdować się bądz to w pliku dyskowym (podaje się wówczas nazwę tego pliku),
bądz w pamięci (podaje się wówczas wskaznik odpowiedniego obszaru). Zawartość strumienia TBlobStream
jest więc kopiowana do pomocniczego strumienia pamięciowego TMemoryStream; może on być traktowany
tak, jakby jego właściwość Memory stanowiła wskaznik do obszaru zawierającego zawartość  i ten właśnie
wskaznik przekazywany jest jako argument wywołania funkcji PlaySound().
W momencie rozpoczęcia kopiowania pomiędzy strumieniami zmieniony zostaje domyślny wygląd kursora
(cfDefault) na postać sygnalizującą zajętość programu (crHourGlass); przywrócenie standardowej postaci
kursora następuje dopiero po zakończeniu odtwarzania, zwalniane są wówczas także strumienie TBlobStream i
TMemoryStream.
Kompletny kod modułu głównego projektu przedstawiony jest na wydruku 7.4.
Wydruk 7.4. Moduł głównego formularza projektu Wavez.dpr
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, DBCtrls, DB, DBTables, StdCtrls, Mask, Buttons, ComCtrls;
type
TMainForm = class(TForm)
tblSounds: TTable;
dsSounds: TDataSource;
tblSoundsWaveTitle: TStringField;
tblSoundsWave: TBlobField;
edTitle: TDBEdit;
edFileName: TDBEdit;
Label1: TLabel;
Label2: TLabel;
OpenDialog: TOpenDialog;
tblSoundsFileName: TStringField;
SaveDialog: TSaveDialog;
pnlToobar: TPanel;
sbPlay: TSpeedButton;
sbAdd: TSpeedButton;
sbSave: TSpeedButton;
sbExit: TSpeedButton;
Bevel1: TBevel;
dbnNavigator: TDBNavigator;
stbStatus: TStatusBar;
procedure sbPlayClick(Sender: TObject);
procedure sbAddClick(Sender: TObject);
procedure sbSaveClick(Sender: TObject);
procedure sbExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
procedure OnAppHint(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
uses MMSystem;
procedure TMainForm.sbPlayClick(Sender: TObject);
var
B: TBlobStream;
M: TMemoryStream;
begin
B := TBlobStream.Create(tblSoundsWave, bmRead); // utwórz strumień BLOB
Screen.Cursor := crHourGlass; // kursor sygnalizujący
// oczekiwanie
try
M := TMemoryStream.Create; // utwórz strumień pamięciowy
try
M.CopyFrom(B, B.Size); // kopiuj ze strumienia BLOB
// do strumienia pamięciowego
// Spróbuj odtworzyć plik dzwiękowy; wygeneruj wyjątek,
// gdy coś się nie powiedzie
Win32Check(PlaySound(M.Memory, 0, SND_SYNC or SND_MEMORY));
finally
M.Free;
end;
finally
Screen.Cursor := crDefault;
B.Free; // zwolnij strumień
end;
end;
procedure TMainForm.sbAddClick(Sender: TObject);
begin
if OpenDialog.Execute then
begin
tblSounds.Append;
tblSounds['FileName'] := ExtractFileName(OpenDialog.FileName);
tblSoundsWave.LoadFromFile(OpenDialog.FileName);
edTitle.SetFocus;
end;
end;
procedure TMainForm.sbSaveClick(Sender: TObject);
begin
with SaveDialog do
begin
FileName := tblSounds['FileName']; // nadaj nazwę pliku
if Execute then // dialog
tblSoundsWave.SaveToFile(FileName); // zapisz BLOB w pliku
end;
end;
procedure TMainForm.sbExitClick(Sender: TObject);
begin
Close;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.OnHint := OnAppHint;
tblSounds.Open;
end;
procedure TMainForm.OnAppHint(Sender: TObject);
begin
stbStatus.SimpleText := Application.Hint;
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
tblSounds.Close;
end;
end.
Filtrowanie danych
Filtrowanie danych polega na ograniczeniu widoczności rekordów zbioru danych wyłącznie do tych rekordów,
które spełniają określone kryterium; umożliwia ono także prostą realizację wyszukiwania rekordów. Wszystko to
odbywa się za sprawą (wyłącznie) kodu w Object Pascalu. Wyszukiwanie za pomocą filtrowania ma tę zaletę, iż
nie wymaga obecności indeksów; chociaż na ogół przebiega ono mniej efektywnie niż to za pomocą indeksów
(czym zajmiemy się w dalszej części niniejszego rozdziału), jest jednak bardzo przydatne w niemal wszystkich
aplikacjach bazodanowych.
Zawężenie zbioru widocznych rekordów na podstawie określonego kryterium następuje poprzez wykonanie
następujących czynności:
1. Oprogramowania zdarzenia OnFilterRecord zbioru danych  zdarzenie to generowane jest dla każdego
rekordu w zbiorze, natomiast procedura jego obsługi decyduje o widoczności (lub niewidoczności)
badanego rekordu.
2. Ustawienia na True właściwości Filtered zbioru danych.
W charakterze przykładu przeanalizujmy tabelę customer.db bazy DBDEMOS  fragment jej
nieprzefiltrowanej zawartości przedstawia rysunek 7.11.
Rysunek 7.11. Zbiór danych w postaci nieprzefiltrowanej
Ograniczymy teraz zestaw widocznych rekordów tylko do tych, w których nazwa firmy (w polu Company)
rozpoczyna się od litery S; w tym celu musimy przypisać zdarzeniu OnFilterRecord następującą procedurę
zdarzeniową:
procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
var
FieldVal: String;
begin
// pobierz wartość pola Company
FieldVal := DataSet['Company'];
// zaakceptuj rekord, gdy zawartość pola zaczyna się od "S"
Accept := (FieldVal<>'') and (FieldVal[1] = 'S');
end;
Po ustawieniu na True właściwości Filtered i uruchomieniu projektu, zobaczymy tylko wybrane rekordy (rys.
7.12):
Rysunek 7.12. Rekordy stanowiące wynik filtrowania
Wskazówka
Alternatywnym sposobem filtrowania rekordów jest wykorzystanie właściwości Filter, zawierającej formułę
określającą widoczność rekordu. Jest ona mniej uniwersalna niż zdarzenie OnFilterRecord  wszak nie
każde kryterium daje się zapisać za pomocą skondensowanej metody  ma jednak pewne następstwa dla
efektywności aplikacji. Na przykład w bazach danych SQL komponent TTable przekazuje (podczas otwarcia)
do serwera zawartość swej właściwości Filtered w klauzuli WHERE zapytania SQL, co zazwyczaj powoduje
szybsze ustalenie właściwego podzbioru rekordów niż w przypadku przeszukiwania zbioru rekord po
rekordzie.
Przeszukiwanie zbiorów danych
Istnieje wiele sposobów poszukiwania rekordów w zbiorach danych. Techniki opisywane w niniejszym
podrozdziale są charakterystyczne raczej dla lokalnych zbiorów danych, nie korzystających z mechanizmów
SQL. Metody wyszukiwania oparte na języku SQL opisane zostały w rozdziale 28. książki  Delphi 4.
Vademecum profesjonalisty .
FindFirst() i FindNext()
Klasa TDataSet definiuje metody FindFirst(), FindNext(), FindPrior() i FindLast() ustawiające
zbiór danych na (odpowiednio) pierwszym, następnym, poprzednim i ostatnim rekordzie spełniającym kryterium
określone przez procedurę obsługi zdarzenia OnFilterRecord. Metody te są funkcjami bezparametrowymi,
zwracającymi wartość typu Boolean, informującą, czy żądany rekord został znaleziony.
Lokalizowanie rekordu za pomocą metody Locate()
Filtrowanie można także wykorzystać do znalezienia konkretnego rekordu na podstawie określonej zawartości
jego wybranych pól. Temu celowi służy metoda Locate() klasy TDataSet  ponieważ opiera ona swe
działanie na mechanizmie filtrowania, nie jest zależna od istnienia konkretnych indeksów, chociaż jest w stanie
wykorzystywać indeksy zgodne z kryterium poszukiwania.
Metoda TDataSet.Locate() zadeklarowana jest następująco:
function Locate( const KeyFields: string;
const KeyValues: Variant;
Options: TLocateOptions): Boolean;
Pierwszy parametr (KeyFields) zawiera oddzielone średnikami nazwy pól (w szczególności  pojedynczą
nazwę) biorących udział w wyszukiwaniu. Drugi parametr (KeyValues) określa żądane wartości tych pól,
natomiast treścią trzeciego parametru (Options) są dodatkowe opcje precyzujące scenariusz poszukiwania:
Type
TLocateOption = (loCaseInsensitive, loPartialKey);
TLocateoptions = set of TLocateOption;
Opcja loCaseInsensitive powoduje niewrażliwość na wielkość liter w porównaniach; użycie opcji
loPartialKey spowoduje uznanie pola za zgodne z porównywanym wzorcem nawet wtedy, gdy wzorzec ten
stanowi część (podłańcuch) jego zawartości.
Poniższa instrukcja jest poleceniem znalezienia rekordu, który w polu o nazwie CustNo posiada wartość 1356:
Table1.Locate('CustNo', 1356, []);
Metoda Locate zwraca wartość True, gdy żądany rekord zostanie znaleziony i False w przeciwnym razie.
Wskazówka
Metoda Locate ma tę przewagę nad innymi metodami wyszukiwania, iż zawsze usiłuje wybrać
najefektywniejszą metodę wyszukiwania, na przykład dokonując chwilowego przełączenia aktywnych
indeksów. Nie jest jednak od tych indeksów zależna w żaden sposób i nie narzuca aplikacji żadnych
ograniczeń w kwestii gospodarowania indeksami dla innych celów.
Przeszukiwanie tabeli za pomocą indeksów
W niniejszym punkcie opiszemy podstawowe właściwości i metody komponentu TTable, reprezentującego
tabelę bazy danych. Szczególną uwagę zwrócimy przy tym na zastosowanie indeksów do celów wyszukiwania
rekordów, jak również do definiowania zakresów (ranges), stanowiących jedną z odmian filtrowania.
Wyszukiwanie rekordów w tabelach
VCL oferuje kilka metod przeszukiwania tabel. W stosunku do tabel systemów dBase i Paradox Delphi
zakłada istnienie indeksów zdefiniowanych na polach określających kryterium poszukiwania. Nie ma takiego
wymagania w stosunku do baz SQL, chociaż brak indeksu zgodnego z kryterium wyszukiwania może niekiedy
pogorszyć znacząco jego efektywność.
Na przykład załóżmy, iż zamierzamy znalezć rekord posiadający określone wartości w dwóch polach 
numerycznym i alfanumerycznym. Możemy to zrobić wykorzystując metodę FindKey() albo parę metod
SetKey()& GotoKey().
FindKey()
Zadaniem metody FindKey()jest znalezienie rekordu zawierającego ściśle określone wartości w określonych
polach. Jedynym parametrem wywołania  typu array of const  jest tablica wartości wymaganych
w tych właśnie polach; sam zestaw pól nie jest specyfikowany w wywołaniu metody, gdyż jest on już
zdeterminowany przez aktywny indeks. Na przykład, w celu znalezienia rekordu, zawierającego w polach
indeksujących wartości (kolejno) 123 i Hello, można użyć następującego wywołania:
RecordFound := Table1.FindKey([123, 'CustNo']);
Wynik funkcji  True albo False  informuje o tym, czy żądany rekord został znaleziony. W przypadku
nieznalezienia rekordu bieżąca pozycja tabeli pozostaje niezmieniona.
Jeżeli liczba podanych wartości pól jest mniejsza niż liczba pól indeksowych, brakujące wartości przyjmuje się
jako puste (NULL), tak więc np. wywołanie
Table1.FindKey([123]);
równoważne jest
Table1.FindKey([123, '']);
Nietrudno spostrzec, iż używanie metody FindKey() wymaga, by pamiętać kolejność pól w aktywnym
indeksie. Nie ma tej wady drugie ze wspomnianych rozwiązań.
SetKey()& GotoKey()
Wyszukiwanie za pomocą pary wywołań Setkey()...GotoKey()ma charakter nieco inny  proces
wyszukiwania przebiega dwuetapowo. Wywołanie metody SetKey()wprowadza tabelę w specyficzny stan
(dsSetKey); w tym stanie poszczególnym polom indeksującym zostają przyporządkowane kolejne wartości  te
same, które w wywołaniu FindKey()podawane były w skondensowanej, tablicowej postaci. Właściwe
poszukiwanie rozpoczyna się w momencie wywołania metody GotoKey(). Oto przykład równoważny
poprzedniemu:
with Table1 do
begin
// pierwszy etap
SetKey;
Fields[0].AsInteger := 123;
Fields[1].AsString := 'Hello';
// drugi etap
RecordFound := GotoKey;
end;
Numery pól odnoszą się tutaj do ich kolejności w indeksie, nie w rekordzie  numer 0 oznacza pierwsze pole
indeksujące, itp.
Uważny czytelnik mógłby zapytać w tym miejscu, dlaczego instrukcja
Fields[0].AsInteger := 123;
nie powoduje przypisania wartości 123 pierwszemu polu indeksującemu? Skąd komponent TTable wie, iż jest
to definiowanie kryterium wyszukiwania?
Otóż wywołanie metody SetKey() powoduje specyficzne przełączenie buforów  mamy więc faktycznie do
czynienia z zapisem wartości do pól, nie są to jednak pola bieżącego rekordu, lecz tzw. rekordu kluczowego.
Wywołanie metody GotoKey() przywraca pierwotny stan buforów.
Opisane rozwiązanie nie uwalnia nas jeszcze od konieczności pamiętania kolejności pól w indeksie; konieczność
ta zniknie jednak, jeżeli w miejsce numerów pól będziemy się posługiwać ich nazwami:
with Table1 do
begin
// pierwszy etap
SetKey;
Fields['Factory'].AsInteger := 123;
Fields['Passwd'].AsString := 'Hello';
// drugi etap
RecordFound := GotoKey;
end;
Dozwolone jest jednak używanie wyłącznie pól indeksujących; mimo wszystko musimy pamiętać ich zestaw.
I jeszcze ciekawostka  ponieważ obydwie opisane metody wykonują w gruncie rzeczy to samo zadanie,
można by podejrzewać między nimi jakiś związek w bibliotece VCL. Istotnie:
function TTable.FindKey(const KeyValues: array of const): Boolean;
begin
CheckBrowseMode;
SetKeyFields(kiLookup, KeyValues);
Result := GotoKey;
end;
Wyszukiwanie przybliżone  kryterium najlepszego dopasowania
Metody FindKey()oraz SetKey()...GotoKey() poszukują rekordu, którego zawartości pól są dokładnie
równe wartościom poszukiwanym. Delphi oferuje analogiczne mechanizmy dla wyszukiwania przybliżonego,
prowadzącego do znalezienia rekordu o zawartości pól najlepiej pasującej3 do poszukiwanego kryterium. Ich
metodyka jest dokładnie taka sama, inne są tylko nazwy metod, będących notabene procedurami, nie funkcjami:
// pierwszy sposób:
Table1.FindNearest([123, 'CustNo']);
// drugi sposób
with Table1 do
begin
// pierwszy etap
SetKey;
Fields['Factory'].AsInteger := 123;
Fields['Passwd'].AsString := 'Hello';
// drugi etap
GotoNearest;
end;
3
Tak naprawdę jest to wartość bezpośrednio większa (w kolejności aktywnego indeksu) od szukanej  podobnie jak przy
poszukiwaniu SOFTSEEK ON w dBase (przyp. tłum.).
Jeśli poszukiwanie zostanie uwieńczone sukcesem i właściwość KeyExclusive zbioru danych równa będzie
False, bieżącym rekordem bazy stanie się rekord znaleziony; jeżeli KeyExclusive równa będzie True,
rekordem bieżącym stanie się rekord następny (w kolejności indeksu) po znalezionym.
Wskazówka
Wszędzie, gdzie to jest możliwe, należy używać metod FindKey()/FindNearest() zamiast
Setkey()& GotoKey()/GotoNearest(), ponieważ te ostatnie wymagają więcej kodowania i jako takie są
bardziej podatne na błędy programisty.
Który indeks?
Dotychczas zakładaliśmy milcząco, że aktywnym indeksem jest tzw. indeks główny tabeli (primary index).
Użycie w tej roli innego indeksu wymaga odpowiedniego ustawienia właściwości IndexName, zawierającej
nazwę indeksu. Oto przykład użycia, jako aktywnego, indeksu po polu Company, nazwanego byCompany w
celu poszukiwania rekordu, w którym pole to ma wartość 'Unisco':
with Table1 do
begin
IndexName := 'ByCompany';
SetKey;
FieldValues['Company'] := 'Unisco';
GotoKey;
end;
Wskazówka
Przełączeniu aktywnego indeksu towarzyszą pewne czynności dodatkowe, które mogą powodować
kilkusekundową zwłokę w działaniu aplikacji.
Połączeniem filtrowania z indeksowaniem jest w Delphi mechanizm zakresów (ranges). Do zakresu rekordów
należy każdy rekord zbioru danych, którego wyrażenie kluczowe (rozumiane jako konkatenacja pól
indeksujących) nie wykracza poza zadane wartości graniczne. Definiowanie zakresu może odbywać się
jednoetapowo, za pomocą metody SetRange(), bądz  na raty , za pomocą metod SetRangeStart(),
SetRangeEnd() i ApplyRange().
Ostrzeżenie
Dla tabel dBase a i Paradoxa pola definiujące zakres muszą być polami indeksującymi. Dla baz SQL-a
warunek ten nie musi być spełniony, lecz wówczas może pogorszyć się efektywność filtrowania.
SetRange()
Podobnie jak metody FindKey() i FindNearest(), metoda SetRange() umożliwia kompleksowe
wykonanie złożonego zadania. Parametrami jej wywołania są dwie tablice array of const, z których
pierwsza określa dolne ograniczenia pól indeksujących, natomiast druga  górne ich ograniczenia. Poniższa
instrukcja ustanawia zakres rekordów, w których wartość pierwszego pola indeksującego jest nie mniejsza od 10
i nie większa od 15:
Table1.SetRange([10],[15]);
ApplyRange()
Definiowanie zakresu można także rozłożyć  na raty  poszczególne etapy scenariusza wyglądają wówczas
następująco:
1. Wywołanie metody SetRangeStart().
2. Przypisanie polom indeksującym dolnych ograniczeń zakresu.
3. Wywołanie metody SetRangeEnd().
4. Przypisanie polom indeksującym górnych ograniczeń zakresu.
5. Wywołanie metody ApplyRange().
Poprzedni przykład wykorzystujący metodę SetRange() można zapisać w następującej, równoważnej postaci:
with Table1 do
begin
SetRangeStart;
Fields[0].AsInteger := 10; // dolna granica zakresu
SetRangeEnd;
Fields[0].AsInteger := 15; // górna granica zakresu
ApplyRange; // zatwierdzenie zakresu
end;
Do usunięcia zdefiniowanego zakresu i przywrócenia zbiorowi danych stanu sprzed wywołania SetRange()
lub ApplyRange() służy metoda CancelRange():
Table1.CancelRange;
Moduły danych
Moduły danych (datamodules) umożliwiają skoncentrowanie wszelkich reguł i zależności związanych z bazami
danych w pojedynczych obiektach, nadających się do współdzielenia pomiędzy formularzami, projektami,
grupami czy nawet przedsiębiorstwami. Moduł danych reprezentowany jest w Delphi przez klasę TDataModule
i jej klasy pochodne. Jest rodzajem niewidocznego formularza, grupującego wszelkie komponenty projektu
związane z dostępem do danych. Fizyczne utworzenie jego obiektu odbywa się przez wybranie pozycji Data
Module ze strony New repozytorium (dostępnej za pośrednictwem opcji File|New|Other).
Podstawową korzyścią wynikającą z używania modułów danych jest możliwość współdzielenia pomiędzy
wieloma formularzami tego samego układu komponentów np. TTable, TQuery i TStoredProc,
ustanawiającego określone powiązania pomiędzy tymi komponentami, czy też specyficzne ograniczenia na
poziomie pojedynczych pól, jak np. wartość minimalna i maksymalna czy format wyświetlania. W ramach
pojedynczego modułu danych można nawet zawrzeć kompletne odzwierciedlenie reguł biznesowych aplikacji
(business rules), specyficznych dla danego przedsiębiorstwa. W przypadku używania wyłącznie  zwykłych
formularzy hermetyzacja taka byłaby mocno utrudniona.
Moduły danych przejawiają zresztą znacznie większą uniwersalność  podobnie jak zwykłe formularze, mogą
być umieszczane w repozytorium i wielokrotnie wykorzystywane w przyszłości na potrzeby tworzonych
projektów. Nabiera to szczególnego znaczenia w warunkach pracy zespołowej  repozytorium powinno
znajdować się wówczas w katalogu dostępnym dla wszystkich zainteresowanych projektantów.
Aby zademonstrować wykorzystanie modułu danych, stworzyliśmy przykładowy projekt, w którym kilka
formularzy odwołuje się do tych samych danych. Nasz moduł danych ma postać wręcz elementarną  w
profesjonalnych aplikacjach bazodanowych moduły danych są znacznie bardziej skomplikowane.
Wyszukiwanie, zakresy, filtrowanie
Aby zilustrować omawiane w tym rozdziale zagadnienia, stworzyliśmy przykładowy projekt SRF.dpr 
znajduje się on na załączonym krążku CD-ROM. Celem tego projektu jest pokazanie prawidłowego użycia
filtrów, wyszukiwania na podstawie indeksów oraz definiowania zakresów. Projekt zawiera kilka formularzy;
formularz główny składa się w zasadzie tylko z przeglądarki wyświetlającej dane, każdy z pozostałych
formularzy zostanie omówiony oddzielnie
Moduł danych
Rozpoczniemy od modułu danych. Moduł ten, nazwany DM, zawiera jedynie komponenty TTable i
TDataSource. Komponent TTable o nazwie Table1 skojarzony jest z tabelą customers.db w bazie
DBDEMOS, komponent TDataSource (o nazwie DataSource1) stanowi zródło dla każdego komponentu
operującego danymi tabeli. Kod zródłowy modułu danych znajduje się w module DataMod.pas.
Formularz główny
Formularz główny  MainForm został przedstawiony na rysunku 7.13; jego plik zródłowy nosi nazwę
Main.pas. Zawiera przeglądarkę DBGrid1 skojarzoną z komponentem DataSource1 modułu danych oraz
przyciski umożliwiające wybór pola kluczowego.
Rysunek 7.13. Formularz główny projektu
Wskazówka
Aby komponent DataSource1 modułu danych dostępny był (na etapie projektowania) dla przeglądarki
umieszczonej w formularzu głównym, należy umieścić nazwę DataMod na liście uses modułu Main.pas.
Najprościej możemy to zrobić otwierając okno modułu Main.Pas w edytorze kodu i wybierając z menu
głównego IDE opcję File|Use Unit; wyświetlona zostanie lista wszystkich modułów projektu, z której
należy wybrać pozycję DataMod. Operację tę należy powtórzyć dla każdego modułu, którego formularz
pobiera informację z modułu danych.
Pole wyboru o nazwie RGKeyField służy do wyboru aktywnego indeksu spośród dwóch istniejących indeksów.
Kod zródłowy związany z tą operacją wygląda następująco:
procedure TMainForm.RGKeyFieldClick(Sender: TObject);
begin
case RGKeyField.ItemIndex of
0: DM.Table1.IndexName := ''; // indeks główny
1: DM.Table1.IndexName := 'ByCompany'; // indeks pomocniczy, po polu Company
end;
end;
Formularz główny zawiera także komponent TMainMenu, umożliwiający otwieranie i zamykanie pozostałych
formularzy. Kompletny tekst modułu Main.pas znajduje się na wydruku 7.5.
Wydruk 7.5. Main.Pas  ilustracja funkcjonowania zakresów
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Grids, DBGrids, DB, DBTables,
Buttons, Mask, DBCtrls, Menus, KeySrch, Rng, Fltr;
type
TMainForm = class(TForm)
DBGrid1: TDBGrid;
RGKeyField: TRadioGroup;
MainMenu1: TMainMenu;
Forms1: TMenuItem;
KeySearch1: TMenuItem;
Range1: TMenuItem;
Filter1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
procedure RGKeyFieldClick(Sender: TObject);
procedure KeySearch1Click(Sender: TObject);
procedure Range1Click(Sender: TObject);
procedure Filter1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
uses DataMod;
{$R *.DFM}
procedure TMainForm.RGKeyFieldClick(Sender: TObject);
begin
case RGKeyField.ItemIndex of
0: DM.Table1.IndexName := ''; // indeks główny
1: DM.Table1.IndexName := 'ByCompany'; // indeks pomocniczy, po polu Company
end;
end;
procedure TMainForm.KeySearch1Click(Sender: TObject);
begin
KeySearch1.Checked := not KeySearch1.Checked;
KeySearchForm.Visible := KeySearch1.Checked;
end;
procedure TMainForm.Range1Click(Sender: TObject);
begin
Range1.Checked := not Range1.Checked;
RangeForm.Visible := Range1.Checked;
end;
procedure TMainForm.Filter1Click(Sender: TObject);
begin
Filter1.Checked := not Filter1.Checked;
FilterForm.Visible := Filter1.Checked;
end;
procedure TMainForm.Exit1Click(Sender: TObject);
begin
Close;
end;
end.
Notatka
Zwróć uwagę na następującą instrukcję w module Rng.Pas:
DM.Table1.SetRange([StartEdit.Text], [EndEdit.Text]);
Wartości graniczne zadane są tutaj w postaci tekstowej, tymczasem jeden z indeksów opiera się na polu
CustNo zawierającym liczbę całkowitą. Nie ma w tym jednak żadnej niekonsekwencji  po prostu metody
SetRange(), FindKey() i FindNearest()samoczynnie dokonują niezbędnej konwersji pomiędzy
wartościami znakowymi i numerycznymi, zwalniając tym samym programistę od selektywnego stosowania
funkcji w rodzaju IntToStr() i StrToInt().
Formularz szukania na podstawie indeksów
Formularz ten nosi nazwę KeySearchForm i realizuje funkcje poszukiwania rekordu na podstawie wyrażenia
indeksowego związanego z aktywnym indeksem. Jego kod zródłowy znajduje się w pliku KeySrch.Pas.
Oprócz szukanej wartości możliwe jest także określenie dwóch aspektów poszukiwania: po pierwsze, możliwe
jest wyszukiwanie w sposób przyrostowy (incremental)  poszukiwanie zostaje wówczas przeprowadzone po
każdej zmianie pola Szukaj:. Podczas wyszukiwania normalnego poszukiwanie rozpoczyna się dopiero po
kliknięciu jednego z przycisków Dokładne i Przybliżone, regulujących (jak łatwo wywnioskować) drugi
aspekt wyszukiwania.
Kompletny kod modułu KeySrch.Pas przedstawia wydruk 7.6.
Wydruk 7.6. Kod zródłowy modułu KeySrch.Pas
unit KeySrch;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TKeySearchForm = class(TForm)
Panel1: TPanel;
Label3: TLabel;
SearchEdit: TEdit;
RBNormal: TRadioButton;
Incremental: TRadioButton;
Label6: TLabel;
ExactButton: TButton;
NearestButton: TButton;
procedure ExactButtonClick(Sender: TObject);
procedure NearestButtonClick(Sender: TObject);
procedure RBNormalClick(Sender: TObject);
procedure IncrementalClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
procedure NewSearch(Sender: TObject);
end;
var
KeySearchForm: TKeySearchForm;
implementation
uses DataMod, Main;
{$R *.DFM}
procedure TKeySearchForm.ExactButtonClick(Sender: TObject);
begin
{
Spróbuj znalezć rekord, dla którego wyrażenie indeksowe równe jest
dokładnie poszukiwanej wartości. Zauważ, iż Delphi w razie potrzeby
przeprowadza automatyczną konwersję pomiędzy znakową, a numeryczną
postacią pola.
}
if not DM.Table1.FindKey([SearchEdit.Text]) then
MessageDlg(Format('Brak rekordu dla "%s" .', [SearchEdit.Text]),
mtInformation, [mbOk], 0);
end;
procedure TKeySearchForm.NearestButtonClick(Sender: TObject);
begin
{
Znajdz rekord najlepiej pasujący do poszukiwanego. Ponownie
pamiętaj o automatycznej konwersji pomiędzy numeryczną a znakową
postacią pola
}
DM.Table1.FindNearest([SearchEdit.Text]);
end;
procedure TKeySearchForm.NewSearch(Sender: TObject);
{
Ta metoda uruchamiana jest pośrednio przy zmianie pola edycyjnego,
jeżeli włączone jest szukanie przyrostowe
}
begin
DM.Table1.FindNearest([SearchEdit.Text]); // szukaj tekstu
end;
procedure TKeySearchForm.RBNormalClick(Sender: TObject);
begin
ExactButton.Enabled := True; // odblokuj przyciski Dokładny/Przybliżony
NearestButton.Enabled := True;
SearchEdit.OnChange := Nil; // zablokuj zdarzenie OnChange
end;
procedure TKeySearchForm.IncrementalClick(Sender: TObject);
begin
ExactButton.Enabled := False; // zablokuj przyciski Dokładny/Przybliżony
NearestButton.Enabled := False;
SearchEdit.OnChange := NewSearch; // odblokuj zdarzenie OnChange
NewSearch(Sender); // szukaj wg bieżącej zawartości pola
// edycyjnego
end;
procedure TKeySearchForm.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
Action := caHide;
MainForm.KeySearch1.Checked := False;
end;
end.
Jak już wspominaliśmy, Delphi automatycznie dokonuje ewentualnych niezbędnych konwersji pomiędzy
znakową a numeryczną postacią argumentów metod FindKey() i FindNearest(). Argumenty te pobierać
więc można bezpośrednio z pola edycyjnego. Na krótki komentarz zasługuje też realizacja szukania
przyrostowego. W tym trybie szukania przyciski Dokładne i Przybliżone są zablokowane, po każdej zmianie
zawartości pola edycyjnego przeprowadzane jest szukanie przybliżone  generowane zdarzenie OnChange
wywołuje procedurę obsługi NewSearch() wywołującą metodę FindNearest(). W trybie szukania
normalnego przyciski Dokładne i Przybliżone są dostępne,  zaślepione jest natomiast zdarzenie OnChange pola
edycyjnego.
Formularz filtrowania
Formularz filtrowania składa się z dwóch części. Górna część demonstruje poszukiwanie pierwszego, ostatniego,
kolejnego lub poprzedniego rekordu w warunkach aktywności lub nieaktywności filtru, którym jest określona
zawartość pola Ogranicz do stanu związana z polem State. Dolna część związana jest z metodą Locate() 
dwa pola edycyjne umożliwiają wybór pola stanowiącego kryterium poszukiwania oraz podanie żądanej wartości
tego pola. Grupa Dopasowanie umożliwia wybór i określenie opcji wyszukiwania: wybranie poszukiwania
przybliżonego powoduje włączenie opcji loPartialKey, natomiast brak zaznaczenia pola Rozróżniaj małe/duże
litery włącza opcję loCaseInsensitive.
Po każdorazowej zmianie opcji Wykonaj filtrowanie (komponent cbFiltered), jej stan kopiowany jest do
właściwości Filtered tabeli Table1 w module danych:
DM.Table1.Filtered := cbFiltered.Checked;
Jeżeli kopiowana wartość równa jest True (opcja jest zaznaczona), tabela Table1 wykonuje filtrowanie
posługując się następującą procedurą zdarzeniową (z modułu DataMod.pas):
procedure TDM.Table1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
{
zaakceptuj rekord, jeżeli zawartość pola edycyjnego
w formularzu filtrowania równa jest zawartości pola
State tabeli, reprezentowanego przez komponent Table1State
}
Accept := Table1State.Value = FilterForm.DBEdit1.Text;
end;
W wyniku kliknięcia przycisku Lokalizuj wywoływana jest metoda Locate() wspomnianej tabeli, po
uprzednim ustaleniu opcji loPartialKey i loCaseInsensitive:
procedure TFilterForm.LocateBtnClick(Sender: TObject);
var
LO: TLocateOptions;
begin
LO := [];
if not CBCaseSens.Checked
then
Include(LO, loCaseInsensitive);
if RBClosest.Checked
then
Include(LO, loPartialKey);
if not DM.Table1.Locate(CBField.Text, EValue.Text, LO)
then
MessageDlg('Brak dopasowania', mtInformation, [mbOk], 0);
end;
Nazwa pola, względem którego odbywa się filtrowanie, pobierana jest z listy combo o nazwie CBField; lista ta
kompletowana jest w czasie tworzenia formularza:
procedure TFilterForm.FormCreate(Sender: TObject);
var
i: integer;
begin
with DM.Table1 do begin
for i := 0 to FieldCount - 1 do
CBField.Items.Add(Fields[i].FieldName);
end;
end;
Wskazówka:
Powyższa procedura ma szansę na poprawne wykonanie tylko wtedy, gdy w momencie tworzenia formularza
TFilterForm moduł danych DM jest już kompletny, w przeciwnym razie otrzymamy komunikat o naruszeniu
kontroli dostępu (access violation). Spełnienie tego warunku możemy bardzo łatwo zapewnić, ustalając
właściwą kolejność pozycji na liście formularzy tworzonych automatycznie (Auto create forms), na karcie
Forms opcji projektu: moduł danych DM musi następować zaraz po formularzu głównym MainForm (który musi
być tworzony w pierwszej kolejności).
Kompletny kod modułu formularza filtrowania przedstawia wydruk 7.7.
Wydruk 7.7. Fltr.Pas  moduł formularza filtrowania
unit Fltr;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Buttons, Mask, DBCtrls, ExtCtrls;
type
TFilterForm = class(TForm)
Panel1: TPanel;
Label4: TLabel;
DBEdit1: TDBEdit;
cbFiltered: TCheckBox;
Label5: TLabel;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
SpeedButton3: TSpeedButton;
SpeedButton4: TSpeedButton;
Panel2: TPanel;
EValue: TEdit;
LocateBtn: TButton;
Label1: TLabel;
Label2: TLabel;
CBField: TComboBox;
MatchGB: TGroupBox;
RBExact: TRadioButton;
RBClosest: TRadioButton;
CBCaseSens: TCheckBox;
procedure cbFilteredClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure LocateBtnClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure SpeedButton3Click(Sender: TObject);
procedure SpeedButton4Click(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
end;
var
FilterForm: TFilterForm;
implementation
uses DB, DataMod, Main;
{$R *.DFM}
procedure TFilterForm.cbFilteredClick(Sender: TObject);
begin
{
włącz filtrowanie gdy zaznaczona jest opcja
"wykonaj filtrowanie"
}
DM.Table1.Filtered := cbFiltered.Checked;
end;
procedure TFilterForm.FormCreate(Sender: TObject);
var
i: integer;
begin
with DM.Table1 do begin
for i := 0 to FieldCount - 1 do
CBField.Items.Add(Fields[i].FieldName);
end;
end;
procedure TFilterForm.LocateBtnClick(Sender: TObject);
var
LO: TLocateOptions;
begin
LO := [];
if not CBCaseSens.Checked
then
Include(LO, loCaseInsensitive);
if RBClosest.Checked
then
Include(LO, loPartialKey);
if not DM.Table1.Locate(CBField.Text, EValue.Text, LO)
then
MessageDlg('Brak dopasowania', mtInformation, [mbOk], 0);
end;
procedure TFilterForm.SpeedButton1Click(Sender: TObject);
begin
DM.Table1.FindFirst;
end;
procedure TFilterForm.SpeedButton2Click(Sender: TObject);
begin
DM.Table1.FindNext;
end;
procedure TFilterForm.SpeedButton3Click(Sender: TObject);
begin
DM.Table1.FindPrior;
end;
procedure TFilterForm.SpeedButton4Click(Sender: TObject);
begin
DM.Table1.FindLast;
end;
procedure TFilterForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caHide;
MainForm.Filter1.Checked := False;
end;
end.
Zakładki
Zakładki (bookmarks), zgodnie ze swą nazwą, umożliwiają zapamiętanie bieżącej pozycji zbioru danych i
pózniejszy szybki powrót do niej na żądanie. Aatwość ich wykorzystywania wynika z faktu, iż wymagają
operowania tylko jedną właściwością.
Zakładka reprezentowana jest w Delphi przez typ TBookmarkStr, a realizującą tę koncepcję właściwością
zbioru danych jest Bookmark. Zawiera ona informację o bieżącej pozycji w zbiorze; aby pozycję tę
zapamiętać, należy odczytać wartość właściwości i przechować ją w zmiennej typu TBookmarkStr:
var
BM: TBookMarkStr;
begin
BM := Table1.BookMark;
Zmiana właściwości Bookmark, przez przypisanie jej wartości wcześniej zapamiętanej, spowoduje ustawienie
zbioru danych na pozycji odpowiadającej tej wartości:
Table1.Bookmark := BM;
Ponieważ TBookmarkStr jest synonimem typu String
type
TBookmarkStr = string;
zakładki należą do zmiennych o kontrolowanym czasie życia (nie jest konieczne ich zwalnianie). Gdy jednak
zakładka okazuje się niepotrzebna, możemy zwolnić zajętą przez nią pamięć, przypisując jej łańcuch pusty:
BM := '';
Wybór łańcucha AnsiString jako reprezentacji dla zakładki powoduje jej niezależność od konkretnej
implementacji bazy danych, ponieważ dane zakładki obsługiwane są całkowicie przez BDE.
Wskazówka
Mimo iż nadal w Delphi dostępne są funkcje GetBookmark(), GotoBookmark() i FreeBookmark()
pochodzące jeszcze z wersji Delphi 1, zaleca się używanie właściwości Bookmark i zmiennych
TBookmarkStr jako konstrukcji wygodniejszych i mniej podatnych na błędy.
Funkcjonowanie zakładek można prześledzić na przykładowym projekcie BookmarkDemo.dpr, który
umieściliśmy na załączonym krążku CD-ROM.
Podsumowanie
Celem niniejszego rozdziału było wprowadzenie Czytelnika w problematykę programowania obsługi baz
danych. Przedstawiliśmy komponenty wywodzące się z klasy TDataSet i reprezentujące zróżnicowane
technologicznie zbiory danych. Omówiliśmy sposoby poruszania się po zbiorze danych i manipulowania jego
zawartością. Zilustrowaliśmy także kilkoma przykładami możliwości, jakie niesie ze sobą filtrowanie i
indeksowanie, w szczególności  różnorodne sposoby wyszukiwania rekordów.
W kolejnych rozdziałach zajmiemy się nową w Delphi 6 technologią dbExpress oraz mechanizmem dbGo,
umożliwiającym dostęp do danych typu ADO.


Wyszukiwarka

Podobne podstrony:
04 (131)
2006 04 Karty produktów
04 Prace przy urzadzeniach i instalacjach energetycznych v1 1
04 How The Heart Approaches What It Yearns
str 04 07 maruszewski
[W] Badania Operacyjne Zagadnienia transportowe (2009 04 19)
Plakat WEGLINIEC Odjazdy wazny od 14 04 27 do 14 06 14
MIERNICTWO I SYSTEMY POMIAROWE I0 04 2012 OiO
04 kruchosc odpuszczania rodz2
Rozdział 04 System obsługi przerwań sprzętowych
KNR 5 04
egzamin96 06 04

więcej podobnych podstron