r07 03 GVTRBPHN53DY2ZMZUVOKXW2IBV4AIK7I2JT5XNY


1
Rozdział 7.
Tworzenie własnych
komponentów
2
Możliwości oferowane przez rodzime komponenty biblioteki VCL są naprawdę imponujące i dla
wielu programistów  projektantów stanowią one materiał w zupełności wystarczający do
tworzenia skomplikowanych aplikacji. Ze względu jednak na ogrom rozmaitych zastosowań
technologii informatycznych zupełnie naturalną może okazać się sytuacja, kiedy to żaden z
istniejących komponentów nie będzie w pełni przydatny do realizacji założonego celu
projektowego. W tym kontekście ogromną zaletą C++Buildera (jak również Delphi) jest nie tylko
 otwartość biblioteki VCL, polegająca na możliwości wzbogacania jej o komponenty pochodzące
z niezależnych zródeł, lecz przede wszystkim zestaw oferowanych przez IDE mechanizmów
ułatwiających tworzenie nowych komponentów we własnym zakresie, przy minimalnym (w
porównaniu do osiąganych korzyści) wysiłku.
W rozdziale tym przedstawimy  na przykładach  najważniejsze zagadnienia związane z
tworzeniem nowych komponentów, a więc wzbogacanie klasy bazowej o niezbędne właściwości,
metody i zdarzenia, różnicowanie zachowań komponentu w zależności od uwarunkowań
zewnętrznych (np. uruchomiona aplikacja kontra etap projektowania), rejestrację gotowych
komponentów w palecie, i oczywiście związane z tym mechanizmy IDE.
Rozdział ten nie wyczerpuje bynajmniej szerokiej tematyki definiowania nowych komponentów.
Nie zagłębialiśmy się na przykład w mechanizmy warstwy Win32 API, o których zainteresowany
Czytelnik przeczytać może w rozdziale 14. Mimo iż komponenty rejestrowane w środowisku IDE
rezydują w ramach konkretnych pakietów, pominęliśmy również tematykę tych ostatnich 
piszemy o nich nieco obszerniej w rozdziale 11. Szczegóły wielu interesujących zagadnień opisane
są także w plikach systemu pomocy C++Buildera i Windows SDK.
Podstawy tworzenia komponentów
Poszczególne komponenty VCL różnią się od siebie pod względem architektury, genealogii,
spełnianych funkcji itp., więc przy tworzeniu nowego komponentu sprawą niezmiernie istotną jest
wybór właściwej klasy bazowej.
Komponenty niewidoczne (non-visual) definiowane są zazwyczaj na bazie klasy
TComponent. Jako klasa bazowa dla wszystkich komponentów zapewnia ona środki dla
integracji komponentów ze środowiskiem IDE, jak również dla strumieniowania ich właściwości
(informacje na temat strumieniowania i obiektów trwałych znajdziesz w rozdziale 6.).
Podstawowym przeznaczeniem komponentów niewidocznych jest obudowywanie fragmentów
kodu spełniających określone funkcje, nie pozostające w bezpośredniej relacji do konkretnej
reprezentacji wizualnej. Przykładem takiej funkcji może być przechwytywanie komunikatów o
błędach i kierowanie ich do jakiejś kontrolki  tekstowej w rodzaju TMemo czy TRichEdit,
bądz zapisywanie ich w pliku tekstowym; spełniający tę funkcję konkretny komponent wykonuje
swoje czynności niejako  w tle , bez manifestowania się w konkretnej postaci graficznej.
Komponenty okienkowe (windowed) wywodzą się z klasy TWinControl. Pojawiają się one w
czasie wykonania aplikacji jako elementy jej interfejsu graficznego i zapewniają interakcję z
użytkownikiem, w postaci np. wyboru określonej pozycji z listy bądz wpisywania zawartości
tekstowej w pola edycyjne. Jakkolwiek możliwe jest wyprowadzanie komponentów okienkowych
bezpośrednio z klasy TWinControl, C++Builder oferuje specjalnie w tym celu klasę
TCustomControl.
3
Komponenty graficzne (graphic) tym różnią się od komponentów okienkowych, iż nie posiadają
uchwytu (handle), reprezentującego okno systemu Windows; nie umożliwiają więc bezpośredniej
interakcji z użytkownikiem, mogą jednak reagować na komunikaty stanowiące skutek określonych
zachowań użytkownika, np. klikania myszą. Brak wspomnianego uchwytu ma również pewne
pozytywne konsekwencje w postaci mniejszego zapotrzebowania na zasoby systemu. Komponenty
graficzne budowane są najczęściej na bazie klasy TGraphicControl.
Rozszerzanie możliwości klasy bazowej
Tworzenie nowych komponentów drogą  rozbudowy komponentów istniejących jest naturalną
konsekwencją obiektowej natury komponentów VCL  dziedziczenie i polimorfizm czynią kod
obiektu możliwym do wielokrotnego użycia (ang. reusable) w tym sensie, iż elementy klasy
bazowej  pola, właściwości, metody  nie wymagają ponownego programowania, lecz są z tej
klasy dziedziczone przez klasę pochodną. Posiada to niebagatelne konsekwencje, chociażby w
kontekście poprawności tego kodu  dziedzicząc bowiem należycie przetestowany kod klasy
bazowej dziedziczy się jednocześnie jego wiarygodność; nie da się tego oczywiście powiedzieć o
kodzie tworzonym  od zera .
Okazuje się, iż tworzenie nowego komponentu niekoniecznie musi wiązać się z definiowaniem
nowych pól, właściwości i metod; równie częstą przesłanką w tym względzie jest zmiana
standardowych ustawień danego komponentu. Wyjaśnijmy tę prostą koncepcję na równie prostym
przykładzie.
Najpowszechniej bodaj używanymi komponentami we wszystkich niemal aplikacjach są etykiety
(TLabel), służące zazwyczaj do opisywania innych komponentów. Umieszczając na formularzu
nowy komponent TLabel i spoglądając na jego właściwości, zauważamy, iż jego tytuł
(Caption) tożsamy jest z nazwą (Name) i wypisany domyślną, ośmiopunktową czcionką MS
Sans Serif w kolorze czarnym. Zmiana tych domyślnych ustawień  stosownie do wymagań
użytkownika tworzącego aplikację  nie stanowi oczywiście żadnego problemu, staje się jednak po
trosze uciążliwa, gdy dokonywać jej trzeba permanentnie, kilkanaście  kilkadziesiąt razy w
każdej nowo tworzonej aplikacji. Można zaoszczędzić sobie tej fatygi, definiując nowy
komponent, różniący się od komponentu bazowego TLabel jedynie wartościami początkowymi
niektórych właściwości; wartości te nadawane będą stosownym właściwościom w treści
konstruktora.
Pokażemy teraz, jak łatwo jest wykonać tę czynność w praktyce. Rozpoczynamy od wybrania
opcji Component|New Component z menu głównego IDE. W wyświetlonym oknie
dialogowym wybieramy żądaną klasę bazową (w polu Ancestor type)  w tym przypadku
TLabel; najprościej wpisać w tym celu kilka początkowych liter tej nazwy (np.  TLa ), i
dokonać rozwinięcia listy skojarzonej z polem edycyjnym. Po kliknięciu żądanej pozycji
wspomnianej listy wypełnione zostaną dodatkowo pola: Class Name, Palette Page i Unit
file name, zawierające (odpowiednio): proponowaną nazwę klasy tworzonego komponentu,
nazwę strony palety komponentów, na której nowy komponent zostanie umieszczony, a także
nazwę i lokalizację pliku modułu zródłowego. Zmieniając nazwę tworzonej klasy  w tym
przypadku na TStyleLabel  spowodujemy automatyczną zmianę proponowanej nazwy
modułu zródłowego.
Gdy klikniemy przycisk OK, C++Builder dokona automatycznego wygenerowania modułu
zródłowego związanego z tworzonym komponentem (patrz wydruki 7.1 i 7.2). Jedyną niezbędną z
4
naszej strony ingerencją w wygenerowany tekst modułu będzie uzupełnienie konstruktora
komponentu o instrukcje dokonujące niezbędnych ustawień początkowych, co na wydruku 7.2
zaznaczone zostało tekstem wytłuszczonym  domyślną czcionką dla nowo umieszczanych na
formularzach komponentów TStyleLabel będzie 12-punktowa wytłuszczona czcionka
Verdana.
Wydruk 7.1. StyleLabel.h  wygenerowany plik nagłówkowy dla nowo tworzonego
komponentu
//---------------------------------------------------------------------------
#ifndef StyleLabelH
#define StyleLabelH
//---------------------------------------------------------------------------
#include
#include
#include
#include
#include
//---------------------------------------------------------------------------
class PACKAGE TStyleLabel : public TLabel
{
private:
protected:
public:
__fastcall TStyleLabel(TComponent* Owner);
__published:
};
//---------------------------------------------------------------------------
#endif
Wydruk 7.1. StyleLabel.cpp  wygenerowany tekst modułu zródłowego dla nowo
tworzonego komponentu
//---------------------------------------------------------------------------
#include
#pragma hdrstop
#include "StyleLabel.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TStyleLabel *)
{
new TStyleLabel(NULL);
}
//---------------------------------------------------------------------------
__fastcall TStyleLabel::TStyleLabel(TComponent* Owner)
: TLabel(Owner)
{
5
Font >Name = "Verdana";
Font >Size = 12;
Font >Style = Font >Style << fsBold;
}
//---------------------------------------------------------------------------
namespace Stylelabel
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TStyleLabel)};
RegisterComponents("Samples", classes, 0);
}
}
//---------------------------------------------------------------------------
Komponent TStyleLabel jest już gotowy do zainstalowania w palecie. Fizycznej instalacji
dokonuje się, wybierając opcję Component|Install Component z menu głównego IDE.
Komponenty C++Buildera rezydują wewnątrz pakietów; użytkownik ma do wyboru instalację w
istniejącym pakiecie (służy do tego karta Into Existing Package) lub we własnym
pakiecie o wybranej nazwie i stosownym opisie (co umożliwia karta Into New Package).
Kliknięcie przycisku OK spowoduje rozpoczęcie przez C++Builder właściwych czynności
instalacyjnych  należy wówczas odpowiedzieć twierdząco na zapytanie o utworzenie nowego
pakietu i poczekać na wyświetlenie komunikatu końcowego, zawierającego nazwę utworzonego
pakietu i nazwę klasy utworzonego komponentu.
Podobną w swej istocie przesłanką tworzenia nowych komponentów jest nie tyle zmiana ich
właściwości domyślnych, ile zmiana kategorii widoczności poszczególnych właściwości, a
dokładniej  zmiana zestawu właściwości opublikowanych (published), a więc dostępnych z
poziomu inspektora obiektów. Przykładem takiej operacji może być ukrycie (na etapie
projektowania) właściwości Items komponentu TListBox, która to właściwość stanie się tym
samym dostępna jedynie z poziomu kodu aplikacji. Operację tę można najłatwiej wykonać,
wybierając w charakterze klasy bazowej TCustomListBox  zestaw jej opublikowanych
właściwości ogranicza się do opublikowanych właściwości dziedziczonych z klasy
TWinControl; użytkownik ma więc pełną swobodę wyboru (do opublikowania) właściwości
nowo definiowanych. W taki właśnie sposób zdefiniowano klasę TListBox.
Założenia projektowe
Podobnie jak w przypadku tworzenia aplikacji, tak i przy tworzeniu komponentów należy wykazać
się dostateczną dozą wyobrazni w zakresie przyszłych tendencji rozwojowych powstającego
produktu. I tak na przykład, decydując się na tworzenie szerokiego wachlarza rozmaitych list
przeglądowych, podobnych do TListBox i bazujących na materiałach zródłowych jakiegoś
specyficznego rodzaju, nie należy pochopnie wyprowadzać każdej z tych list (jako komponentu)
bezpośrednio z klasy TListBox, lecz zastanowić się nad jakimś specyficznym komponentem
wyprowadzonym właśnie z TListBox (lub TCustomListBox), obejmującym pewne cechy
wspólne dla wszystkich tworzonych (teraz i w przyszłości) list przeglądowych. Oczywiście, nie
6
będąc jasnowidzem, trudno jest owe cechy a priori bezbłędnie przewidzieć  i właśnie w tym celu
niezbędna jest wspomniana przed chwilą wyobraznia.
Niezwykle pomocnym w zrozumieniu zależności pomiędzy komponentami okaże się z pewnością
schemat biblioteki VCL (VCL Chart), stanowiący wyposażenie C++Buildera. Dzięki niemu
widoczne staje się natychmiast dziedziczenie poszczególnych właściwości, modyfikowanie
zachowań wynikających z przedefiniowywania metod wirtualnych itp.; wiedza ta może być
wzbogacona przez lekturę kodu zródłowego biblioteki (w języku Object Pascal), dostępnego w
plikach *.pas, znajdujących się w drzewie podkatalogu Source lokalnej instalacji
C++Buildera.
Tworzenie komponentów niewidocznych
Świat komponentów VCL zbudowany jest na trzech solidnych fundamentach: właściwościach,
zdarzeniach i metodach. W tym podrozdziale zajmiemy się ich rolą w funkcjonowaniu
poszczególnych komponentów i organizowaniu pomiędzy nimi współpracy warunkującej
efektywne funkcjonowanie budowanych aplikacji.
Właściwości
Właściwości komponentów podzielić można na dwie grupy, w zależności od ich dostępności dla
programisty operującego z poziomu IDE: właściwości opublikowane (published) dostępne są
poprzez inspektora obiektów, właściwości niepublikowane (non-published) dostępne są tylko z
poziomu kodu zródłowego.
Właściwości niepublikowane
Spójrzmy na poniższą deklarację klasy:
Wydruk 7.3. Metody służące do odczytu i modyfikacji prywatnego pola klasy
class LengthClass
{
private:
int FLength;
public:
LengthClass(void){}
~LengthClass(void){}
int GetLength(void);
void SetLength(int pLength);
void LengthFunction(void);
}
7
Klasa LengthClass posiada prywatną zmienną FLength, której wartość może być
odczytywana i zmieniana za pomocą metod (odpowiednio) GetLength() i SetLength(), na
przykład w ten sposób:
Wydruk 7.4. Dostęp do prywatnego pola klasy
LengthClass Rope;
Rope.SetLength(15);
....
int NewLength = Rope.GetLength();
Powyższy kod nie odwołuje się bezpośrednio do pola FLength, wykorzystując w zamian
wspomniane metody. W złożonej aplikacji może to ujemnie wpływać na czytelność kodu 
związek pomiędzy polem FLength a metodami GetLength() i SetLength() nie jest
widoczny na pierwszy rzut oka, ponadto z samą czynnością odczytywania i zmieniania wartości
czegokolwiek bardziej kojarzy się operator przypisania niż wywołanie funkcji. C++Builder
(podobnie jak Delphi) udostępnia w związku z tym mechanizm właściwości (ang. properties) 
deklaracja z wydruku 7.3 może zostać przepisana w sposób następujący:
Wydruk 7.5. Właściwość organizująca dostęp do prywatnego pola klasy
class LengthClass2
{
private:
int FLength;
public:
LengthClass2(void){}
~LengthClass2(void){}
void LengthFunction(void);
__property int Length = {read = FLength, write = FLength};
}
Dostęp do pola FLength będzie mieć wówczas postać bardziej intuicyjną:
Wydruk 7.6. Zmiana pola klasy za pomocą właściwości
LengthClass Rope;
Rope.Length = 15;
....
int NewLength = Rope.Length;
8
Zamiast wywołań funkcji mamy do czynienia z odczytem i zapisem  czegoś o nazwie Length,
co w swej naturze przypomina pole klasy, lecz polem bynajmniej nie jest: słowo kluczowe
__property oznacza, iż mamy do czynienia z właściwością. Klauzula read w definicji
właściwości Length informuje, iż jej odczyt powinien być fizycznie zrealizowany jako odczyt
zmiennej FLength; analogicznie ma się sprawa z zapisem, za który odpowiedzialna jest klauzula
write. Na pierwszy rzut oka nie wydaje się to szczególnie odkrywcze, lecz w porównaniu z
bezpośrednim dostępem do pola FLength różni się co najmniej pod dwoma względami:
" w przeciwieństwie do pola, właściwość można uczynić tylko odczytywalną, opuszczając
klauzulę write w jej definicji;
" sposób dostępu do wybranych elementów klasy oddzielony jest od ich fizycznej
implementacji, ta ostatnia może więc być zmieniana bez potrzeby jakichkolwiek
modyfikacji w kodzie odwołującym się do obiektów tej klasy.
Mechanizm właściwości umożliwia coś więcej. Otóż właściwość jest ze swej natury tworem dość
abstrakcyjnym  dopiero jej definicja wiąże ją z  konkretnym elementem klasy, jakim na
wydruku 7.5 jest pole FLength; w związku z tym odczyt lub zmiana jej wartości mogą wymagać
czynności bardziej skomplikowanych niż tylko odczyt lub zamiana któregoś z pól. Elementem
wskazywanym przez klauzule read i write może więc być także metoda klasy, jak na wydruku
7.7:
Wydruk 7.7. Właściwość wykorzystująca metody dostępowe
class LengthClass3
{
private:
int FLength;
int GetLength(void);
void SetLength(int pLength);
public:
LengthClass3(void){}
~LengthClass3(void){}
void LengthFunction(void);
__property int Length = {read = GetLength, write = SetLength};
}
Fraza read = GetLength oznacza tu, iż wynikiem odczytu właściwości Length jest wynik
zwrócony przez metodę GetLength(), tak więc na przykład instrukcja:
int NewLength = Rope.Length;
realizowana jest fizycznie jako:
int NewLength = Rope.GetLength();
9
Podobnie przypisanie do właściwości nowej wartości jest tylko symbolicznym oznaczeniem
czegoś, co tak naprawdę jest wywołaniem metody  zgodnie z definicją na wydruku 7.7
przypisanie:
Rope.Length = 15;
realizowane jest fizycznie jako:
Rope.SetLength(15);
gdzie przypisywana wartość jest parametrem wywołania metody specyfikowanej w klauzuli
write. Ze względu na rolę pełnioną w klasie LengthClass3 metody GetLength() i
SetLength() nazywane są metodami dostępowymi (ang. access methods), organizują one
bowiem dostęp do właściwości Length.
 Obliczenia wykonywane przez metody dostępowe mogą być niekiedy dość złożone 
przykładowo zmiana właściwości komponentu bazodanowego, reprezentującej konkretną bazę
danych, określoną za pomocą aliasu, wymaga rozłączenia się z dotychczasową bazą i przyłączenia
do innej. Czynności te realizowane są jednak  w tle przez metody dostępowe  programista
ogranicza się tylko do przypisania właściwości nowej nazwy aliasu.
Typy właściwości
Właściwości klas mogą być dowolnego typu, na przykład: int, bool, short itp., mogą także
same być klasami. W tym ostatnim przypadku muszą one czynić zadość dwóm wymaganiom: po
pierwsze, jeżeli dana właściwość ma mieć charakter trwały  tj. zapisywana ma być w strumieniu
 musi się ona wywodzić (bezpośrednio lub pośrednio) z klasy TPersistent, definiującej
niezbędne ku temu mechanizmy; po drugie, jeżeli klasa właściwości deklarowana jest w formie
zapowiedzi (ang. forward), zapowiedz ta musi zawierać klauzulę:
_declspec(delphiclass)1.
Spójrzmy na wydruk 7.8, ilustrujący typową deklaracje zapowiadającą  zapowiadana klasa nie
jest wykorzystywana przez żadną właściwość:
Wydruk 7.8. Deklaracja zapowiadająca
class MyClass;
1
Jak wiadomo z rozdziału 8., klauzulę __declspec(delphiclass) zastąpić można
wywołaniem makra DELPHICLASS, zdefiniowanego w pliku nagłówkowym sysmac.h 
przyp. tłum.
10
class PACKAGE MyComponent : public TComponent
{
private:
MyClass *FMyClass;
....
};
class MyClass : public TPeristent
{
public:
__fastcall MyClass(void){}
};
Jeżeli jednak zdefiniujemy właściwość typu MyClass, w deklaracji zapowiadającej musi pojawić
się klauzula __declspec(delphiclass), co ilustruje wydruk 7.9:
Wydruk 7.9. Zapowiedz klasy wykorzystywanej jako typ właściwości
class __declspec(delphiclass) MyClass;
class PACKAGE MyComponent : public TComponent
{
private:
MyClass *FMyClass;
....
__published:
__property MyClass *Class1 = {read = FMyClass, write = FMyClass};
};
class MyClass : public TPeristent
{
public:
__fastcall MyClass(void){}
};
Makro PACKAGE jest tutaj odpowiednikiem klauzuli __declspec(package), oznaczającej,
iż deklarowana klasa jest komponentem zdolnym do przechowywania w ramach pakietów.
Właściwości opublikowane
Aby właściwość komponentu dostępna była za pośrednictwem inspektora obiektów, należy ją
wpierw opublikować, czyli umieścić jej definicję w sekcji __published deklaracji klasy.
Opublikowane właściwości są oczywiście także dostępne w kodzie programu, na równi z
pozostałymi właściwościami, manipulowanie ich wartościami za pomocą inspektora obiektów jest
jednak na ogół wygodniejsze niż wpisywanie równoważnych temu instrukcji w kod programu.
Właściwości opublikowane są także domyślnie utrwalane w strumieniu, w wyniku czego nadane
11
im ostatnio (na etapie projektowania) wartości pozostają aktualne przy następnym załadowaniu
projektu do IDE lub uruchomieniu skompilowanej aplikacji.
Aby więc opublikować właściwość Length klasy z wydruku 7.7, należy przesunąć jej definicję z
sekcji public do sekcji __published:
Wydruk 7.10. Opublikowanie właściwości
class PACKAGE LengthClass : public TComponent
{
private:
int FLength;
int GetLength(void);
void SetLength(int pLength);
public:
__fastcall LengthClass(TObject *Owner) : TComponent(Owner) {}
__fastcall ~LengthClass(void){}
void LengthFunction(void);
__published:
__property int Length = {read = Getlength, write = Setlength};
}
Opublikowana właściwość nie będzie jednak widoczna w oknie inspektora obiektów, jeżeli w jej
definicji brak będzie klauzuli write. Nie będzie można wówczas zmienić jej wartości, a więc
udostępnianie jej przez inspektora obiektów i tak byłoby bezcelowe. Można jednak pogodzić dwa
pozornie sprzeczne wymagania, pozostawiając właściwość widoczną dla inspektora obiektów i
jednocześnie chroniąc ją przed zmianą wartości  inspektor obiektów nie wnika bowiem w to, co
dzieje się wewnątrz metody dostępowej, można więc wskazać w klauzuli write funkcję o
pustej treści. Wydruk 7.11 ilustruje pewną odmianę tej idei  metoda dostępowa
SetVersion() właściwości Version nadaje tej ostatniej wartość określoną a priori, nie
zaś wskazaną przez użytkownika; parametr tej metody nie jest w ogóle wykorzystywany, a więc
dla uniknięcia ostrzeżeń ze strony kompilatora został on  wykomentowany . W efekcie
właściwość Version posiada wciąż niezmienną wartość określoną przez stałe MajorVersion
i MinorVersion.
Wydruk 7.11. Nietypowa metoda dostępowa
const int MajorVersion = 1;
const int MinorVersion = 0;
class PACKAGE LengthClass : public TComponent
{
private:
AnsiString FVersion;
int FLength;
int GetLength(void);
void SetLength(int pLength);
void SetVersion(AnsiString /* pVersion */ )
{FVersion = AnsiString(MajorVersion) + ""."" +
12
AnsiString(MinorVersion);}
public:
__fastcall LengthClass(TObject *Owner) : TComponent(Owner)
{SetVersion("""");}
__fastcall ~LengthClass(void){}
void LengthFunction(void);
__published:
__property int Length = {read = Getlength, write = Setlength};
__property AnsiString Version = {read = FVersion, write = SetVersion};
}
Właściwości tablicowe
Typ właściwości nie musi być typem skalarnym  może on być również tablicą. Przykładem
właściwości tablicowej jest właściwość Lines komponentu TMemo, reprezentująca jego
zawartość tekstową w podziale na poszczególne wiersze. Właściwość tablicową definiuje się
niemal tak samo jak właściwość skalarną  z tą różnicą, iż należy wskazać fakt indeksowania i
określić typ indeksu; w przeciwieństwie bowiem do  zwykłych tablic indeksy właściwości
niekoniecznie muszą być liczbami całkowitymi. Wydruk 7.12 przedstawia przykład definicji
dwóch właściwości tablicowych, z którym jedna indeksowana jest łańcuchami AnsiString.
Wydruk 7.12. Przykład właściwości tablicowych
class PACKAGE TStringAliasComponent : public TComponent
{
private:
TStringList RealList;
TStringList AliasList;
__AnsiString __fastcall GetStringAlias(AnsiString RawString);
AnsiString __fastcall GetRealString(int Index);
void __fastcall SetRealString(int Index, AnsiString Value);
public:
__property AnsiString AliasString[AnsiString RawString] =
{read = GetStringAlias};
__property AnsiString RealString[int Index] = {read=GetRealString,
write=SetRealString};
}
Tablicowy charakter właściwości odzwierciedla się także w postaci jej metod dostępowych.
Obydwie metody   odczytująca i  zapisująca  posiadają dodatkowy parametr określający,
który  element właściwości należy odczytać lub zmodyfikować; parametr ten specyfikowany jest
na pierwszej pozycji. Oto definicje deklarowanych powyżej metod dostępowych właściwości
RealString:
Wydruk 7.13. Metody dostępowe właściwości tablicowej
AnsiString __fastcall TStringAliasComponent::GetRealString(int Index)
{
13
if(Index > (RealList->Count -1))
return """";
return RealList->Strings[Index];
}
void __fastcall TStringAliasComponent::SetRealString(int Index,
AnsiString Value)
{
if((RealList->Count - 1) < Index)
RealList->Add(Value);
else
RealList->Insert(Index, Value);
}
Wydruk 7.13 stanowi połączenie oryginalnych listingów 9.13 i 9.14
Odwołania do właściwości tablicowej mają identyczną postać jak odwołania do  zwykłej tablicy
 przy założeniu, iż zmienna StringAlias1 wskazuje na obiekt typu
TStringAliasComponent, poniższa sekwencja dokonuje wyczyszczenia pierwszego
elementu właściwości RealString, o ile jest on identyczny z drugim:
if (StringAlias1 >RealString[0] == StringAlias1 >RealString[1])
StringAlias1 >RealString[0] := "";
Nietrudno zauważyć, iż właściwość RealString zdefiniowana została w oparciu o listę
łańcuchów RealList  elementy tej listy odpowiadają wprost poszczególnym elementom
właściwości, z jedną drobną różnicą:  odczyt poza zakresem listy skutkuje zwróceniem pustego
łańcucha, zaś  zapis poza zakresem realizowany jest jako dopisanie podanego łańcucha na końcu
listy. Widać wyraznie, iż posługiwanie się właściwością może mieć charakter nieco bardziej
elastyczny niż bezpośredni dostęp do pola, na którym właściwość ta bazuje.
Każdy z łańcuchów przechowywanych w liście RealList posiada swój odpowiednik  alias  w
liście AliasList; równoważne łańcuchy przechowywane są w obydwu listach na tych samych
pozycjach. Aby więc odnalezć alias określonego łańcucha, należy wpierw odnalezć jego pozycję
w liście RealList (używając metody TStringList::IndexOf()), a następnie odczytać
analogiczną pozycję w liście AliasList. W taki właśnie sposób zaimplementowana została
tablicowa właściwość AliasString, której indeks jest właśnie łańcuchem; wydruk 7.14
przedstawia jej  odczytującą metodę dostępową. Jeżeli łańcuch podany jako indeks nie występuje
w liście RealList lub nie posiada swego odpowiednika w liście AliasList, to przyjmuje się,
że jest on sam dla siebie aliasem.
Wydruk 7.14. Metoda dostępowa właściwości tablicowej indeksowanej za pomocą łańcucha
AnsiString __fastcall TStringAliasComponent::GetStringAlias(
AnsiString RawString)
{
int RawStringIndex;
RawStringIndex = RealList->IndexOf(RawString);
14
if((RawStringIndex == -1) || (RawStringIndex > (AliasList->Count-1)))
return RawString;
return AliasList->Strings[RawStringIndex];
}
Instrukcja znajdująca alias przykładowego łańcucha ma wobec tego następującą postać:
AniString MyAlias = StringAlias1 >AliasString["My original string"];
Domyślne wartości właściwości
Niektóre właściwości posiadają tę osobliwą cechę, iż ich wartość rzadko kiedy różna jest od
pewnej wartości domyślnej  przykładem tego jest właściwość Tag (klasy TComponent), której
wartość rzadko odbiega od (zwyczajowej) wartości 0. Dla tego rodzaju właściwości opłaca się
zastosować alternatywny sposób jej strumieniowania  zamiast konsekwentnego zapisywania jej
do strumienia przy każdym zapisie obiektu, należy zapisywać ją tylko wówczas, gdy jej wartość
(w momencie zapisu) różna jest od wartości domyślnej. Jednocześnie należy inicjować tę
właściwość jej wartością domyślną w konstruktorze klasy  jeżeli została ona uprzednio zapisana
do strumienia, to odczytana z tegoż strumienia wartość zastąpi wartość domyślną nadaną w
konstruktorze; jeżeli właściwość nie posiada swego obrazu w strumieniu, pozostanie ona ze swą
wartością domyślną.
Domyślną wartość właściwości określa się w jej definicji za pomocą klauzuli default 
przykładowo wspomniana właściwość TComponent::Tag definiowana jest następująco:
__property int Tag = {read=FTag, write=FTag, default=0};
Brak klauzuli default oznacza, iż z daną właściwością nie jest skojarzona żadna wartość
domyślna. Konsekwencje klauzuli default (tj. istnienie wartości domyślnej i jej wartość)
dziedziczone są z klasy bazowej w jej klasach pochodnych. Możliwa jest zmiana dziedziczonej
wartości domyślnej za pomocą ponownej specyfikacji dziedziczonej właściwości (w klasie
pochodnej) z użyciem klauzuli default; można także anulować sam fakt istnienia wartości
domyślnej, opatrując dziedziczoną właściwość klauzulą nodefault.
Decyzja o zapisywaniu konkretnej właściwości do strumienia może być również uzależniona od
spełnienia pewnego warunku; warunek ten specyfikowany jest przy użyciu klauzuli stored i
może mieć postać stałej true lub false nazwy pola boolowskiego albo nazwy bezparametrowej
metody, zwracającej wynik typu Boolean. Oto przykład:
Wydruk 7.15. Przykład użycia klauzuli stored
class PACKAGE LengthClass : public TComponent
{
protected:
15
int FProp;
bool StoreProperty(void);
__published:
__property int AlwaysStore = {read = FProp, write = FProp, stored = true};
__property int NeverStore = {read = FProp, write = FProp, stored = false};
__property int SimetimesStore = {read = FProp, write = FProp,
stored = StoreProperty};
}
Kolejność tworzenia właściwości
Jeżeli poszczególne właściwości są od siebie w jakiś sposób uzależnione, istotna jest kolejność ich
odczytywania ze strumienia. Kolejność ta jest identyczna z kolejnością dyrektyw __property
definiujących poszczególne właściwości. Na poniższym wydruku poszczególne właściwości
inicjalizowane są w kolejności: PropA, PropB, PropC  jeżeli więc na przykład metoda
SetPropB() korzystać będzie z właściwości PropC, to właściwość PropB może nie zostać
prawidłowo zainicjowana.
Wydruk 7.16. Kolejność inicjowania właściwości
class PACKAGE SampleComponent : public TComponent
{
private:
int FPropA;
bool FPropB;
String FProC;
void __fastcall SetPropB(bool pPropB);
void __fastcall SetPropC(String pPropC);
public:
__property int PropA = {read = FPropA, write = FPropA};
__property bool PropB = {read = FPropB, write = SetPropB};
__property String PropC = {read = FPropC, write = SetPropC};
}
Zdarzenia
W kategoriach terminologii komponentów VCL zdarzeniem (ang. event) nazywamy wywołanie
określonej metody w reakcji na wystąpienie pewnej okoliczności, którą może być otrzymanie
przez komponent komunikatu od Windows, zaistnienie wyjątku lub wykonanie przez aplikację
pewnej szczególnej czynności.
W charakterze przykładowego komponentu operującego zdarzeniami rozpatrzmy deklarowany na
wydruku 7.17 komponent TTraverseDir. Dokonuje on iterowania po wszystkich
podkatalogach wskazanego katalogu, umożliwiając wykonanie dla każdego z odwiedzanych
katalogów pewnej czynności określonej przez użytkownika. Zgodnie z poprzednim akapitem
wywołanie metody realizującej ową czynność będzie zdarzeniem generowanym w reakcji na
określoną akcję, jaką jest przejście do innego katalogu.
16
Wydruk 7.17. Deklaracja właściwości zdarzeniowej
class PACKAGE TTraverseDir : public TComponent
{
private:
AnsiString FCurrentDir;
TNotifyEvent *FOnDirChanged;
public:
__fastcall TTraverseDir(TObject *Owner) : TComponent(Owner){
FOnDirChanged = 0;}
__fastcall ~TTraverseDir(void){}
__fastcall Execute();
__published:
__property AnsiString CurrentDir = {read = FCurrentDir};
__property TNotifyEvent OnDirChanged = {read = FOnDirChanged,
write = FOnDirChanged};
}
Funkcja, której wywołanie jest istotą wspomnianego zdarzenia  zwana z tego względu funkcją
obsługi zdarzenia (ang. event handler) lub krótko funkcją zdarzeniową  jest tutaj
wskazywana przez pole FOnDirChanged  obudowane właściwością OnDirChanged;
właściwość ta, jako opublikowana, pojawi się na stronie Events inspektora obiektów2. Oto
schemat ilustrujący wywoływanie wskazywanej funkcji przy każdej zmianie katalogu bieżącego:
Wydruk 7.18. Generowanie zdarzenia OnDirChanged w reakcji na zmianę katalogu bieżącego
void __fastcall TTraverseDir::Execute(void)
{
while ()
{


// katalog został zmieniony, wygeneruj zdarzenie
if (FOnDirChanged) // czy określono funkcję obsługi zdarzenia?
{
FOnDirChanged(this);
}
....
}
....
}
2
Inspektor obiektów traktuje daną właściwość jako zdarzeniową (ang. event property), jeżeli stwierdzi, że typ
powoływanego przez nią pola (lub metody) jest wskazaniem na metodę obiektu. Takim typem jest m.in.
TNotifyEvent  przyp. tłum.
17
Funkcja obsługi zdarzenia typu TNotifyEvent posiada pojedynczy parametr wskazujący
obiekt, w związku z którym zdarzenie to jest generowane:
typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender);
Gdy klikniemy dwukrotnie pozycję odpowiadającą zdarzeniu OnDirChanged (na stronie
Events inspektora obiektów) edytor kodu wygeneruje automatycznie stosowny szkielet funkcji
obsługującej to zdarzenie:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender)
{
}
Parametr Sender umożliwia rozróżnienie pomiędzy poszczególnymi komponentami w sytuacji,
gdy kilka z nich posługuje się tą samą funkcją zdarzeniową; nazwa bieżącego katalogu znajduje
się w polu FCurrentDir wskazywanego obiektu:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender)
{
AnsiString ThisDirectory = dynamic_cast(Sender)->FCurrentDir;
if (Sender == Traverse1)
{
akcja charakterystyczna dla komponentu Traverse1
}
else
{
akcja stosowna dla innych komponentów
}
}
Definiowanie własnych typów zdarzeń
Pokażemy teraz, jak zdefiniować własny typ zdarzenia. Procedura obsługi naszego nowego
zdarzenia posiadać będzie drugi parametr typu bool; przy wejściu do funkcji zdarzeniowej będzie
on miał wartość false  zmiana tej wartości na true spowoduje, iż iteracja zostanie przerwana,
tj. następne katalogi nie będą już odwiedzane.
Zdefiniujmy więc najpierw rzeczony typ zdarzenia:
typedef void __fastcall (__closure *TDirChangeEvent)
(System::TObject* Sender, bool &Abort)
a następnie zmodyfikujmy odpowiednio deklarację klasy TTraverseDir:
18
Wydruk 7.19. Zdarzenie o typie definiowanym przez użytkownika
typedef void __fastcall (__closure *TDirChangedEvent)(
System::TObject* Sender, bool &Abort)
class PACKAGE TTraverseDir : public TComponent
{
private:
AnsiString FCurrentDir;
TDirChangedEvent *FOnDirChanged;
public:
....
__published:
__property TDirChangedEvent OnDirChanged = {read = FOnDirChanged,
write = FOnDirChanged};
}
Gdy klikniemy teraz odpowiednią pozycję na stronie Events inspektora obiektów,
wygenerowany szkielet funkcji zdarzeniowej przedstawiał się będzie następująco:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender, bool
&Abort)
{
}
Sama iteracja stanie się również trochę bardziej skomplikowana:
Wydruk 7.20. Iterowanie po katalogach z możliwością przerwania przez obsługę zdarzenia
void __fastcall TTraverseDir::Execute(void)
{
bool AbortIteration = false;
while ()
{


// katalog został zmieniony, wygeneruj zdarzenie
if (FOnDirChanged) // czy określono funkcję obsługi zdarzenia?
{
FOnDirChanged(this, AbortIteration);
if (AbortIteration)
{

break;
}
19
}
....
}
....
}
Metody
Metody komponentów VCL nie różnią się niczym od metod pozostałych klas. Są one funkcjami
C++, spełniającymi pewne specyficzne zadania związane z komponentem; ze względu jednak na
prostotę konstrukcji komponentu i wygodę jego użytkownika używanie metod powinno być
podporządkowane następującym regułom:
" funkcjonowanie komponentu nie może być uzależnione od konieczności jawnego
wywołania (przez użytkownika) którejś z metod  wszelkie niezbędne czynności
inicjalizacyjne powinny być wykonane w ramach konstruktora;
" nie należy wymagać od użytkownika wywoływania metod komponentu w jakiejś
określonej kolejności  nawet jeżeli kolejność taka podyktowana jest względami logiki
działania komponentu, należy obsłużyć każdą sytuację, w której nie będzie ona
przestrzegana. I tak na przykład skierowanie do bazy danych zapytania SQL musi być
poprzedzone jej otwarciem, nie można jednak zakładać, iż użytkownik wywołując
metodę generującą to zapytanie, na pewno wywoła wcześniej metodę ustanawiającą
połączenie z bazą  jeżeli więc zapytanie skierowane zostanie do bazy nie otwartej,
należy fakt ten odpowiednio obsłużyć, na przykład generując wyjątek lub też
automatycznie otwierając bazę;
" metody zmieniające stan komponentu powinny każdorazowo sprawdzać, czy zmiana taka
jest w danej chwili dozwolona w kontekście innych operacji wykonywanych przez
komponent.
Ogólnie rzecz biorąc, preferowanym środkiem komunikowania się komponentu z otoczeniem są
jego właściwości, jak wiadomo również korzystające z metod. Przykładowo komponenty
bazodanowe wywodzące się z klasy TDataSet posiadają właściwość Active, określającą, czy
nawiązane jest połączenie z bazą danych (true), czy też nie (false). Przypisywanie tej
właściwości wartości true i false ma dokładnie taki sam skutek, jak wywoływanie metod
(odpowiednio) Open() i Close(), tak więc dwie poniższe instrukcje są sobie równoważne:
Database1 >Active = true;
Database1 >Open();
podobnie jak instrukcje:
20
Database1 >Active = false;
Database1 >Close();
Decydując się na używanie właściwości Active, można obejść się bez metod Open() i
Close(), z drugiej strony te ostatnie nie mogą zastąpić właściwości Active (bo jak wówczas
sprawdzić stan połączenia komponentu z bazą?).
Metody publiczne i chronione
Metody komponentów tworzone są najczęściej jako publiczne (public) i chronione
(protected). Metody publiczne przeznaczone są do bezpośredniego wywoływania przez
użytkownika; jako że może on realizować te wywołania w dowolnym czasie, należy upewnić się,
iż dana metoda nie powoduje długotrwałego zajęcia procesora, bez chwili  oddechu dla systemu
Windows. Takie  blokujące metody nie tylko paraliżują mobilność aplikacji, lecz równie często
powodują irytację użytkownika, chcącego przerwać czasochłonny proces i nie mogącego tego
uczynić; metoda TTraverseDir::Execute z wydruku 7.20 wykorzystuje bardzo prosty
mechanizm zapobiegający takiej niekorzystnej sytuacji. Równie nieskomplikowanie zapobiec
można sparaliżowaniu aplikacji, wywołując cyklicznie metodę Application
>ProcessMessages().
Metody chronione przeznaczone są w zasadzie na użytek komponentów pochodnych; użytkownik
nie ma do nich bezpośredniego dostępu. Zapobiega to wywoływaniu tych metod w sposób
nieuprawniony, czyli bez spełnienia koniecznych ku temu warunków. Jeżeli możliwości jakiejś
chronionej metody predestynują ją do udostępnienia jej użytkownikowi końcowemu, należy
udostępnienie to zrealizować w sposób pośredni,  obudowując tę chronioną metodę inną metodą
publiczną, stwarzającą swej partnerce warunki niezbędne do jej wywołania lub przynajmniej
badającą spełnienie tych warunków.
Jeżeli metoda chroniona jest metodą dostępową którejś (którychś) właściwości, powinna być
deklarowana jako wirtualna. Umożliwi to modyfikację lub rozszerzenie jej możliwości w klasach
pochodnych. Przykładem takiej wirtualnej metody chronionej jest metoda Loaded() klasy
TComponent wywoływana automatycznie po odczytaniu całego obrazu komponentu ze
strumienia. Jej pierwowzór w klasie TComponent ogranicza się do wyzerowania znacznika
csLoading, oznaczającego, iż właśnie trwa odczyt komponentu:
procedure TComponent.Loaded;
begin
Exclude(FComponentState, csLoading);
end;
natomiast w klasach pochodnych jej treść jest już cokolwiek bardziej złożona. Jako że
poszczególne elementy komponentu  pola, metody i zdarzenia  są od siebie w różnym stopniu
uzależnione, wszelkie czynności  globalne o charakterze np. weryfikacyjnym nie powinny być
podejmowane przed kompletnym załadowaniem jego obrazu ze strumienia. Dla upewnienia się co
21
do spełnienia tego warunku można badać wspomniany znacznik csLoading, można również
zdefiniować w tym celu własną zmienną boolowską. Poniższy wydruk przedstawia typowy
przykład przedefiniowania metody Loaded()  po zrealizowaniu wszystkich czynności
związanych z ukończeniem ładowania komponentu bazowego realizowane są czynności
specyficzne dla klasy pochodnej, m.in. ustawienie wspomnianej zmiennej.
Wydruk 7.21. Przedefiniowanie wirtualnej metody chronionej
class PACKAGE TAliasComboBox : public TSmartComboBox
{
private:
bool IsLoaded;
protected:
virtual void __fastcall Loaded(void);
}
....
void __fastcall TAliasComboBox::Loaded(void)
{
TComponent::Loaded();
if(!ComponentState.Contains(csDesigning))
{
IsLoaded = true;
GetAliases();
}
}
Definiowanie wyjątków związanych z komponentem
Przy tworzeniu własnych komponentów nie można oczywiście zapominać o prawidłowej obsłudze
różnego rodzaju błędnych sytuacji. Owymi  błędnymi sytuacjami są przede wszystkim wyjątki
pojawiające się podczas realizacji kodu związanego z komponentem, są nimi także rozmaite
przypadki niespełnienia wymaganych warunków, wykryte przez metody komponentu.
Najprostszą formą obsługi wyjątku zaistniałego w trakcie realizacji metody komponentu jest
ponowienie tegoż wyjątku, by mógł być on obsłużony w ramach aplikacji wykorzystującej
komponent. Rozwiązanie takie nie zapewnia jednak należytej kontroli nad samym komponentem;
bardziej eleganckim wyjściem wydaje się więc przekształcenie wyjątku w zdarzenie, czyli
sprowadzenie obsługi wyjątku do generowania pewnego specyficznego zdarzenia. Właściwą
obsługę wyjątku zapewniałby wówczas sam użytkownik w ramach obsługi tegoż zdarzenia;
nieprzypisanie wspomnianemu zdarzeniu obsługującej go funkcji mogłoby wówczas skutkować
wspomnianym ponowieniem wyjątku.
22
Należy przestrzec jednak przed nadużywaniem tej koncepcji  zbyt duża liczba zdarzeń
generowanych w reakcji na występowanie wielu wyjątków z pewnością uczyniłaby korzystanie z
komponentu zbyt niewygodnym.
Przesłanką generowania wspomnianego zdarzenia mogą być zresztą nie tylko zaistniałe wyjątki,
lecz i innego rodzaju sytuacje, błędne z punktu widzenia logiki komponentu. W charakterze
przykładu rozpatrzmy komponent MultiQuery, służący do generowania ciągu zapytań SQL pod
adresem bazy danych. Zapytania zgrupowane są w liście Queries typu TStrings, a ich
zródłem może być np. komponent TMemo wypełniony przez użytkownika konkretną zawartością;
samo generowanie zapytań jest sprawą metody Execute(). Przykładowa sekwencja instrukcji
korzystających z komponentu mogłaby wyglądać na przykład tak:
MultiQuery >Queries >Assign(Memo1 >Lines);
MultiQuery >Execute();
To wszystko wydaje się proste tak długo, jak długo zapytania realizowane są bezbłędnie; co zrobić
jednak, jeżeli w czasie realizacji któregoś z nich wystąpi wyjątek? Całkowite  zepchnięcie jego
obsługi na użytkownika nie wydaje się najwłaściwsze z punktu widzenia realizacji całego
scenariusza; należy raczej wygenerować pewne specyficzne zdarzenie, w ramach którego
użytkownik będzie miał możliwość (między innymi) zadecydowania o przerwaniu lub
kontynuowaniu tego scenariusza (czego przykład widzieliśmy już przy okazji iterowania po
strukturze katalogów).
Metoda Execute() komponentu MultiQuery posiłkuje się wywołaniami metody
ExecuteItem(), generującej pojedyncze zapytanie określone przez parametr wywołania równy
indeksowi w liście Queries. Użytkownik również posiada dostęp do metody
ExecuteItem(), którą może wywołać z dowolną wartością parametru  także ujemną lub
wykraczającą poza rozmiar listy. Jest to sytuacja ewidentnie błędna z punktu widzenia logiki
komponentu, z punktu widzenia biblioteki VCL wszystko jest jednak w porządku dopóty, dopóki
nie nastąpi odwołanie do samej listy Queries. W przypadku stwierdzenia niepoprawności
parametru należy więc takiemu odwołaniu zapobiec, generując w zamian pewien specyficzny
wyjątek, będący sygnałem o błędzie; wyjątek ten w dalszej kolejności będzie najprawdopodobniej
konwertowany do zdarzenia w sposób wyżej opisany.
Rozpoczniemy od zdefiniowania klasy reprezentującej nasz specyficzny wyjątek:
Wydruk 7.22. Deklaracja klasy-wyjątku użytkownika
class EMultiQueryIndexOutOfBounds : public Exception
{
public:
__fastcall EMultiQueryIndexOutOfBounds(const AnsiString Msg) :
Exception(Msg){}
};
Następnie uzupełnimy metodę ExecuteItem() o niezbędny test parametru i generowanie (w
razie potrzeby) zdefiniowanego wyjątku:
23
Wydruk 7.23. Generowanie wyjątku użytkownika
void __fastcall TMultiQuery::ExecuteItem(int Index)
{
if(Index < 0 || Index >= Queries->Count)
throw EmultiQueryIndexOutOfBounds;
....
}
W oryginale jest błąd (listing 9.24)  w trzeciej instrukcji zamiast
 Index >= Queries->Count
jest
 Index > Queries->Count .
Błąd ten jest powtórzony na listingu 9.25
Opisane postępowanie jest jak najbardziej celowe w odniesieniu do uruchomionej aplikacji; na
etapie projektowania stosowniejszym  i wygodniejszym dla użytkownika  rozwiązaniem wydaje
się jednak wyświetlenie odpowiedniego komunikatu (zamiast generowania wyjątku).
Zmodyfikowaną metodę ExecuteItem(), czyniącą zadość temu wymaganiu, przedstawia
wydruk 7.24.
Wydruk 7.24. Obsługa błędnej sytuacji na etapie projektowania
void __fastcall TMultiQuery::ExecuteItem(int Index)
{
if(Index < 0 || Index >= Queries->Count)
{
if(ComponentState.Contains(csDesigning))
ShowMessage ("Indeks zapytania poza zakresem");
else
throw EmultiQueryIndexOutOfBounds;
}
....
}
Odpowiednik powyższego listingu w oryginale (9.25) jest bezsensowny.
24
Przestrzenie nazw  dyrektywa namespace
W przypadku wykorzystywania w danym projekcie komponentów pochodzących od różnych
wytwórców prawdopodobne staje się wystąpienie konfliktu nazw. Oto przykład użycia dwóch
różnych komponentów  zegarowych :
// w module od pierwszego wytwórcy:
const bool Mode12; // tryb 12 lub 24 godzinny
class PACKAGE TClock1 : public TComponent
{
}
// w module od drugiego wytwórcy:
const bool Mode12; // tryb 12 lub 24 godzinny
class PACKAGE TClock2 : public TComponent
{
}
Nazwa Mode12 jest tu użyta w dwóch różnych znaczeniach.
W języku C++ środkiem zapobiegającym konfliktom nazw są przestrzenie nazw (ang.
namespaces) identyfikowane przez słowo kluczowe namespace.
Powróćmy na chwilę do wydruku 7.1, przedstawiającego wygenerowany automatycznie kod
modułu związanego z nowo tworzonym komponentem TStyleLabel; znajduje się tam
następująca definicja przestrzeni nazw:
Wydruk 7.25. Przykładowa definicja przestrzeni nazw
namespace Stylelabel
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TStyleLabel)};
RegisterComponents("Samples", classes, 0);
}
}
Aby wykluczyć niebezpieczeństwo konfliktu nazw pochodzących z modułów zródłowych różnych
komponentów, należy deklarować tworzony komponent w jego własnej przestrzeni nazw. Należy
w tym celu  otoczyć przestrzenią nazw deklarację klasy komponentu w pliku nagłówkowym 
wydruk 7.26 pokazuje, jak zrobić to z komponentem TStyleLabel zadeklarowanym w pliku
StyleLabel.h:
Wydruk 7.26. Deklarowanie komponentu w jego własnej przestrzeni nazw
25
//---------------------------------------------------------------------------
#ifndef StyleLabelH
#define StyleLabelH
//---------------------------------------------------------------------------
#include
#include
#include
#include
#include
//---------------------------------------------------------------------------
namespace MJF01_NStyleLabel
{
class PACKAGE TStyleLabel : public TLabel
{
private:
protected:
public:
__fastcall TStyleLabel(TComponent* Owner);
__published:
} ;
}
//---------------------------------------------------------------------------
#endif
Deklaracja przestrzeni nazw otacza tu cały kod występujący po dyrektywach #include;
identyfikator przestrzeni nazw utworzony został na podstawie akronimu firmy  autora
komponentu oraz nazwy klasy poprzedzonej literą  N . Sama nazwa klasy mogłaby nie
wystarczyć  identyfikator przestrzeni nazw musi być bowiem unikatowy z globalnego punktu
widzenia.
Obsługa komunikatów
Biblioteka zasadniczo VCL bierze na siebie całą obsługę komunikatów, konwertując większość z
nich do postaci bardziej strawnych dla użytkownika zdarzeń. Nie oznacza to jednak, iż
programista tworzący aplikację za pomocą C++Buildera (lub Delphi) nie ma do tych komunikatów
dostępu  jeżeli wymagają tego specyficzne względy projektu, programista może wziąć na siebie
obsługę niektórych komunikatów.
Jako przykład rozpatrzmy kontrolkę TStringGrid wzbogaconą o możliwość  przeciągania do
niej plików z Eksploratora Windows; tak zmodyfikowaną kontrolkę opatrzyliśmy nazwą
TSuperStringGrid.
Jeżeli dane okno zarejestrowane jest w systemie jako zdolne przyjmować  przeciągane pliki (o
tym dokładniej za chwilę), w momencie  upuszczenia nad nim ikony reprezentującej plik
przeciągnięty z okna Eksploratora następuje wygenerowanie komunikatu WM_DROPFILES;
szczegółowa informacja niesiona przez ten komunikat reprezentowana jest przez strukturę
TWMDropFiles zdefiniowaną w pliku messages.hpp. Właściwa obsługa komunikatu jest
zadaniem metody WmDropFiles naszej kontrolki. Kod zródłowy uwzględniający owe trzy
czynniki obsługi  nazwę komunikatu, nazwę struktury i nazwę metody  generowany jest w
wyniku następującej sekwencji, zwanej popularnie mapą komunikatu:
26
Wydruk 7.27. Przykładowa mapa komunikatu
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles)
END_MESSAGE_MAP(TStringGrid)
Wykorzystane makra BEGIN_MESSAGE_MAP, MESSAGE_HANDLER i END_MESSAGE_MAP
zdefiniowane są w pliku nagłówkowym sysmac.h w sposób następujący:
Wydruk 7.28. Makra dokonujące mapowania komunikatu
#define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) \
{ \
switch (((PMessage)Message)->Msg) \
{
//-----------------------------------------------------------
#define END_MESSAGE_MAP(base) default: \
base::Dispatch(Message); \
break; \
} \
}
//-----------------------------------------------------------
#define MESSAGE_HANDLER VCL_MESSAGE_HANDLER
#define VCL_MESSAGE_HANDLER(msg,type,meth) \
case msg: \
meth(*((type *)Message)); \
break;
//-----------------------------------------------------------
Jak łatwo się zorientować, mapa komunikatów ma postać instrukcji switch i obejmować może
więcej niż jeden komunikat, tj. wykorzystywać kilka wywołań makra MESSAGE_HANDLER
pomiędzy zamykającymi nawiasami BEGIN_MESSAGE_MAP i END_MESSAGE_MAP.
Wspomniana metoda WmDropFiles, odpowiedzialna za obsługę komunikatu WM_DROPFILES,
zdefiniowana jest następująco:
Wydruk 7.29. Obsługa  upuszczenia pliku
void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message)
27
{
char buff[MAX_PATH];
HDROP hDrop = (HDROP)Message.Drop;
POINT Point;
int NumFiles = DragQueryFile(hDrop, -1, NULL, NULL);
TStringList *DFiles = new TStringList;
DFiles->Clear();
DragQueryPoint(hDrop, &Point);
for(int i = 0; i < NumFiles; i++)
{
DragQueryFile(hDrop, i, buff, sizeof(buff));
DFiles->Add(buff);
}
DragFinish(hDrop);
// teraz DFiles zawiera listę nazw przeciągniętych plików
delete DFiles;
}
Wyjaśnienie wszystkich szczegółów związanych z przeciąganiem plików pomiędzy oknami
wykracza poza ramy tego rozdziału  naszym zadaniem było jedynie zaprezentowanie sposobu
przechwytywania komunikatów Windows przez komponenty VCL. Zainteresowany Czytelnik
znajdzie w systemie pomocy dokładny opis wszystkich funkcji wykorzystywanych w kodzie na
wydruku 7.29.
Jak już wcześniej wspominaliśmy, aby w momencie upuszczenia pliku nad oknem generowany
był komunikat WM_DROPFILES, okno to musi zostać zarejestrowane w systemie jako
predestynowane do tej funkcji. Rejestracji takiej (oraz wyrejestrowania) dokonuje funkcja API o
nazwie DragAcceptFiles(). Jej pierwszy parametr jest uchwytem odnośnego okna,
ukrywającym się pod właściwością Handle każdej kontrolki okienkowej; drugi parametr określa,
czy mamy do czynienia z rejestracją (true  okno ma przyjmować przeciągnięte pliki), czy z
wyrejestrowaniem (false). W naszej kontrolce rodzaj żądania określony jest przez właściwość
CanDropFiles, bazującą na prywatnym polu FCanDropFiles, zatem wywołanie funkcji
rejestrującej ma postać:
DragAcceptFiles(Handle, FCanDropFiles)
i następuje przy każdej zmianie wspomnianej właściwości, jak również przy tworzeniu i
ładowaniu formularza.
Etap projektowania a etap wykonania
Pożądane zachowanie poszczególnych komponentów różni się bardzo często w zależności od tego,
czy uczestniczą one w wykonaniu skompilowanej aplikacji (runtime stage), czy też aplikacja ta
znajduje się właśnie na etapie projektowania (designtime stage). Elementem każdego komponentu,
umożliwiającym odróżnienie etapu wykonania od poszczególnych stadiów etapu projektowania,
jest jego właściwość zbiorowa (Set) o nazwie ComponentState. Znaczenie jej
poszczególnych elementów wyjaśnia pokrótce tabela 7.1.
28
Tabela 7.1. Elementy właściwości TComponent::ComponentState
Znacznik Znaczenie
csAncestor
Komponent znajduje się na formularzu stanowiącym klasę bazową
dla aktualnego formularza. Znacznik ten jest ustawiany tylko łącznie
ze znacznikiem csDesigning; do manipulowania nim służy
metoda TComponent::SetAncestor().
csDesigning
Komponent uczestniczy w aplikacji znajdującej się na etapie
projektowania. Znacznik ten jest ustawiany i zerowany przez metodę
TComponent::SetDesigning().
csDesignInstance
Komponent jest komponentem najwyższego poziomu (root) z punktu
widzenia projektanta formularzy  czyli na przykład ramką
(TFrame) , w której umieszcza się aktualnie inne komponenty, lecz
nie funkcjonującą jako odrębny komponent umieszczony na
formularzu. Znacznik ten jest ustawiany tylko łącznie ze znacznikiem
csDesigning;. do manipulowania nim służy metoda
TComponent::SetDesignInstance(). Nowość w wersji 5.
C++Buildera.
csDestroying
Komponent znajduje się w fazie destrukcji. Znacznik ten ustawiany
jest przez metodę TComponent::Destroying().
csFixups
Komponent powiązany jest z nie załadowanym jeszcze
komponentem na innym formularzu. Znacznik ten zerowany jest
przez globalną funkcję GlobalFixupReferences(), gdy
opracowane zostaną wszystkie zależności pomiędzy komponentami
aplikacji.
csFreeNotification
Komponent będzie wysyłał do innych formularzy powiadomienie o
własnej destrukcji. Znacznik ten ustawiany jest przez metodę
TComponent::FreeNotification(). Nowość w wersji 5.
C++Buildera.
csInline
Komponent jest komponentem najwyższego poziomu umieszczonym
na formularzu i podlegającym modyfikacji w czasie projektowania.
Znacznik ten używany jest do identyfikacji zagnieżdżonych ramek
(TFrame) w czasie ładowania i zapisywania komponentu;
modyfikowany jest przez metodę TComponent::SetInline(),
jest również ustawiany w metodzie
TReader::ReadComponent(). Nowość w wersji 5.
C++Buildera.
csLoading
Obiekt TFiler dokonuje właśnie ładowania komponentu. Znacznik
ten ustawiany jest w momencie rozpoczęcia tworzenia egzemplarza
komponentu i ustawiony jest aż do chwili, gdy komponent zostanie
załadowany wraz z komponentami, dla których jest właścicielem;
29
jego zerowania dokonuje metoda TComponent::Loaded(), zaś
ustawiany jest przez metody TReader::ReadComponent() i
TReader::ReadRootComponent().
csReading
Komponent dokonuje właśnie odczytu swej właściwości ze
strumienia. W przeciwieństwie do znacznika csLoading,
ustawionego przez cały czas ładowania komponentu, ustawiany jest
tylko na czas odczytu poszczególnych właściwości. Znacznik ten
modyfikowany jest przez metody
TReader::ReadComponent() i
TReader::ReadRootComponent().
csWriting
Komponent dokonuje zapisu swej właściwości do strumienia.
Znacznik ten modyfikowany jest przez metodę
TWriter::WriteComponent().
csUpdating
Komponent jest właśnie modyfikowany w konsekwencji zmian
poczynionych w formularzu nadrzędnym (bazowym). Znacznik ten
ustawiany jest tylko łącznie ze znacznikiem csAncestor; jego
ustawiania dokonuje metoda TComponent::Updating(), zaś
zerowania  metoda TComponent::Updated().
Spośród powyższych znaczników najprostszym koncepcyjnie i najczęściej wykorzystywanym
przez użytkownika jest oczywiście csDesigning. Główną przesłanką rozróżniania etapów
projektowania i wykonania jest oczywisty fakt, iż na etapie projektowania użytkownik sprawuje
nieporównywalnie większą kontrolę na komponentami; na etapie tym komponenty są jednak z
reguły odseparowane od rzeczywistych danych, z którymi przyjdzie pracować uruchomionej
aplikacji. Naturalną konsekwencją tych różnic są zachowania aplikacji charakterystyczne
wyłącznie dla jednego z etapów, między innymi:
" weryfikacja właściwości komponentu w kontekście powiązania z innymi komponentami;
na etapie wykonania weryfikacja taka nie ma zazwyczaj sensu wobec oczywistego faktu,
iż pozostałe komponenty zwykle są jeszcze niekompletne;
" ostrzeżenia wyświetlane w przypadku ustawienia niepoprawnej wartości właściwości w
inspektorze obiektów;
" podpowiedzi kontekstowe i różnego rodzaju dialogi ułatwiające użytkownikowi wybór
stosownych ustawień poszczególnych właściwości na etapie projektowania.
Powiązania między komponentami
Poszczególne komponenty projektu mogą być ze sobą powiązane za pośrednictwem swych
właściwości. Przykładowo obiekt TDriveComboBox, udostępniający rozwijalną listę napędów
dostępnych w systemie, powiązany jest zazwyczaj z komponentem TDirectoryListBox,
30
reprezentującym drzewo katalogów wybranego napędu; za powiązanie to odpowiedzialna jest jego
właściwość DirList. Sam fakt powiązania komponentów nie jest oczywiście niczym
niezwykłym, najistotniejszym zagadnieniem są bowiem konsekwencje owego powiązania, a
dokładniej  właściwe reagowanie komponentu na zmiany zachodzące w komponentach z nim
powiązanych. Wybierając (w czasie wykonania aplikacji) jeden z napędów w oknie komponentu
TDriveComboBox, spowodujemy automatyczne uaktualnienie zawartości połączonego z nim
komponentu TDirectoryListBox  bez napisania chociażby jednego wiersza kodu.
Aby zademonstrować różne aspekty powiązania komponentów, skonstruowaliśmy prosty
komponent TMsgLog, którego zadaniem jest przekazywanie komunikatów do kontrolek w
rodzaju TMemo lub TRichEdit, ogólnie  kontrolek wywodzących się z klasy
TCustomControl. Deklarację tego komponentu przedstawia wydruk 7.30; nietrudno zauważyć,
iż elementem odpowiedzialnym za powiązanie ze wspomnianymi kontrolkami jest właściwość
LinkEdit, bazująca na prywatnym polu FLinkEdit.
Wydruk 7.30. Deklaracja komponentu TMsgLog
class PACKAGE TMsgLog : public TComponent
{
private:
TCustomMemo *FLinkedEdit;
public:
__fastcall TMsgLog(TComponent* Owner);
__fastcall ~TMsgLog(void);
void __fastcall OutputMsg(const AnsiString Message);
protected:
virtual void __fastcall Notification
(TComponent *AComponent, TOperation Operation);
__published:
__property TCustomMemo *LinkedEdit =
{read = FLinkedEdit, write = FLinkedEdit};
};
Właściwość LinkEdit, jako opublikowana, pojawi się w oknie inspektora obiektów; na liście
komponentów, których wskazanie może stanowić jej wartość, znajdą się przy tym wyłącznie te
komponenty formularza, które wywodzą się z klasy TCustomMemo  o tym przejawie
 inteligencji inspektora obiektów pisaliśmy już w rozdziale 6.
Niewątpliwie wartość nadana właściwości LinkEdit zachowuje swą aktualność tylko do chwili,
w której zwolniony zostanie wskazywany przez nią obiekt; stanowi to wyzwanie pod adresem
wiarygodności całego mechanizmu powiązania komponentów  zwolnienie wskazywanego
komponentu nie może w żadnym wypadku pozostać niezauważone dla naszej kontrolki
TMsgLog. I rzeczywiście takim nie pozostanie, bowiem w momencie wstawiania nowego
komponentu na formularz lub usuwania z niego dowolnego komponentu wszystkie pozostałe
komponenty zostaną powiadomione o tym fakcie poprzez wywołanie metody Notification
każdego z nich. Metoda ta posiada dwa parametry: pierwszy z nich jest wskaznikiem odnośnego
komponentu, drugi zaś informuje, czy mamy do czynienia ze wstawianiem komponentu
31
(opInsert), czy z jego usuwaniem (opRemove)  w tym ostatnim przypadku nasz komponent
powinien  wyzerować swoją właściwość LinkEdit:
Wydruk 7.31. Usuwanie połączenia z usuniętym komponentem
void __fastcall TMsgLog::Notification(TComponent *AComponent,
TOperation Operation)
{
// nie interesują nas operacje inne niż usuwanie komponentu
if(Operation != opRemove)
return ;
// sprawdz, czy usunięcie dotyczy wskazywanego komponentu
// i jeżeli tak, to wyzeruj wskazanie na niego
if(AComponent == FLinkedEdit)
FLinkedEdit = NULL;
}
Przekazaniem kolejnego komunikatu do wskazywanej kontrolki zajmuje się metoda
OutputMsg()  i tu zaczyna się kolejny problem: otóż sposób tego przekazania różny będzie
dla kontrolek TMemo i TRichEdit. Można by co prawda uniknąć tego kłopotu, ograniczając się
tylko do tego, co oferuje klasa TCustomMemo, a więc właściwości Lines; nasz komponent
wykazuje się jednak pewnym stopniem  inteligencji  jeżeli mianowicie komponent wskazywany
przez LinkEdit jest kontrolką klasy TRichEdit lub pochodnej (co łatwo sprawdzić za
pomocą rzutowania dynamicznego), wykorzystana zostanie możliwość ustawienia koloru jego
czcionki. Oto treść metody OutputMsg():
Wydruk 7.32. Metoda przekazująca komunikat
void __fastcall TMsgLog::OutputMsg(const AnsiString Message)
{
TMemo *LinkedMemo = 0;
TRichEdit *LinkedRichEdit = 0;
LinkedMemo = dynamic_cast(FLinkedEdit);
LinkedRichEdit = dynamic_cast(FLinkedEdit);
if (LinkedRichEdit)
{
LinkedRichEdit->Font->Color = clRed;
LinkedRichEdit->Lines->Add(Message);
}
else if (LinkedMemo)
{
LinkedMemo->Lines->Add(Message);
}
}
32
Zamieniłem listingi 9.31 i 9.32, bowiem musiałem przeredagować treść. Oryginalny listing 9.31 (u
mnie 7.32) jest błędny i bezsensowny, więc go poprawiłem jak należy.
Przedstawiony przykład ma charakter wręcz elementarny, jednak w przypadku komponentów
bardziej złożonych, gdzie powiązania są bardziej skomplikowane (jak np. wśród komponentów
bazodanowych), poprawna obsługa wszelkich zależności pomiędzy powiązanymi komponentami
jest sprawą pierwszorzędną. Dobrze zaprojektowany komponent poznaje się bowiem po tym, iż
jego spodziewane funkcjonowanie daje się osiągnąć przy minimum wysiłku.
Nieco bardziej zaawansowanym przejawem powiązania pomiędzy komponentami jest zdolność
reagowania danego komponentu na zdarzenia zachodzące w kontekście komponentów z nim
powiązanych. Kontrolka wskazywana przez właściwość LinkEdit niekoniecznie musi być
kontrolką tylko do odczytu; użytkownik być może będzie miał ochotę uzupełniać zawarte w niej
komunikaty własnymi komentarzami. Gdy po zakończeniu ewentualnej modyfikacji  przełączy
się on na inną kontrolkę, wygenerowane zostanie zdarzenie OnExit (w kontekście kontrolki
dotychczas aktywnej) i ten właśnie moment może zostać przechwycony przez nasz komponent
TMsgLog. Należy w tym celu zapamiętać oryginalny adres funkcji obsługującej zdarzenie
OnExit we wskazywanej kontrolce,  podpiąć adres własnej funkcji zdarzeniowej, a na końcu
treści tej ostatniej wywołać oryginalną funkcję zdarzeniową. Owa  własna funkcja zdarzeniowa
związana będzie z  własnym zdarzeniem typu TNotifyEvent wskazywanym przez prywatne
pole FOnUsersExit  należy więc nieco zmodyfikować deklarację klasy naszego komponentu
(wytłuszczono nowo dodane elementy):
Wydruk 7.33. Zmodyfikowana deklaracja komponentu TMsgLog
class PACKAGE TMsgLog : public TComponent
{
private:
TCustomMemo *FLinkedEdit;
TNotifyEvent *FPrevExit;
TNotifyEvent *FOnUsersExit;
void __fastcall MsgLogOnExit(TObject *Sender);
public:
__fastcall TMsgLog(TComponent* Owner);
__fastcall ~TMsgLog(void);
void __fastcall OutputMsg(const AnsiString Message);
protected:
virtual void __fastcall Notification
(TComponent *AComponent, TOperation Operation);
virtual void __fastcall Loaded(void);
__published:
__property TCustomMemo *LinkedEdit =
{read = FLinkedEdit, write = FLinkedEdit};
33
};
Najlepszym miejscem przeadresowania obsługi zdarzenia OnExit jest oczywiście metoda
Loaded() wywoływana po kompletnym załadowaniu komponentu:
Wydruk 7.34. Przeadresowanie obsługi zdarzenia komponentu powiązanego
void __fastcall TMsgLog::Loaded(void)
{
TComponent::Loaded();
FPrevExit = 0;
if(!ComponentState.Contains(csDesigning))
{
if(FlinkedEdit)
{
FPrevExit = FlinkedEdit->OnExit;
FlinkedEdit->OnExit = MsgLogOnExit;
}
}
}
Dla prostoty ograniczyliśmy się do etapu wykonania skompilowanej aplikacji. Metoda
MsgLogOnExit() wywołuje wpierw  podpiętą funkcję zdarzeniową komponentu TMsgLog
(o ile użytkownik takową zdefiniował  zmienna FOnUsersExit musi być koniecznie
wyzerowana w konstruktorze), przechodząc następnie do oryginalnej funkcji zdarzeniowej (o ile
takowa istnieje):
Wydruk 7.35. Obsługa zdarzenia OnExit komponentu powiązanego
void __fastcall TMsgLog::MsgLogOnExit(TObject *Sender);
{
if (FOnUsersExit)
{
FOnUsersExit(this);
}
if (FLinkedEdit)
{
if (FPrevExit)
{
FPrevExit(FLinkedEdit);
}
}
}
34
Oryginalny listing 9.34 jest bezsensowny, musiałem przeredagować treść, by w ogóle miała ona
sens.
Projektowanie komponentów wizualnych
Komponenty wizualne tym różnią się od swoich niewizualnych partnerów, iż posiadają określoną
reprezentację graficzną, identyczną na obydwu etapach  projektowania i wykonania aplikacji.
Gdy zmienia się któraś z właściwości odpowiedzialnych za tę reprezentację  czy to wskutek
wykonania kodu aplikacji, czy to w konsekwencji operowania mechanizmami inspektora obiektów
 reprezentacja ta musi zostać odtworzona. O ile większość kontrolek  okienkowych stanowi
prostą enkapsulację standardowych kontrolek Windows, o których wygląd należycie troszczy się
sam system operacyjny, to jednak utrzymywanie właściwego wyglądu kontrolek definiowanych
przez użytkownika wymagać może pewnych dodatkowych lub specyficznych działań, które
powinny znalezć odzwierciedlenie w konstrukcji metody obsługującej zdarzenie OnPaint,
generowane w reakcji na otrzymanie przez kontrolkę komunikatu WM_PAINT, nakazującego jej
odtworzenie swego wyglądu.
Jak już wcześniej pisaliśmy, niezmiernie istotnym zagadnieniem przy tworzeniu nowego
komponentu jest właściwy wybór klasy bazowej, konieczne może się więc okazać w związku z
tym przestudiowanie wielu haseł systemu pomocy czy nawet kodu zródłowego biblioteki VCL.
Nie ma bowiem nic gorszego nad smutną konstatację, iż komponent opracowywany z mozołem
przez wiele dni nie posiada tych możliwości, których od niego oczekiwaliśmy.
TCanvas
Obiekt klasy TCanvas jest w bibliotece VCL reprezentantem  okienkowego kontekstu
urządzenia (ang. DC  Device Context). Udostępnia on wygodne narzędzia do rysowania grafiki i
skomplikowanych kształtów geometrycznych. Począwszy od klasy TCustomControl każdy
komponent posiada swego rodzaju  płótno , przedstawiające jego wygląd graficzny  płótno to
ukrywa się pod właściwością Canvas i jest oczywiście obiektem klasy TCanvas.
Poniższy wydruk ilustruje wykonanie na płótnie komponentu prostej operacji graficznej 
narysowanie przekątnej biegnącej z lewego górnego narożnika:
Wydruk 7.36. Rysowanie przekątnej na płótnie komponentu
Canvas->MoveTo(0, 0);
int X = ClientRect.Right;
int Y = ClientRect.Bottom;
Canvas->LineTo(X, Y);
35
Pierwsza z instrukcji ustawia punkt o współrzędnych (0, 0)  czyli lewy górny narożnik  jako
wyróżnioną ( bieżącą ) pozycję. Następnie pod zmienne X i Y podstawiane są wymiary płótna,
stanowiące jednocześnie współrzędne prawego dolnego narożnika. Wreszcie metoda
LineTo()rysuje odcinek linii prostej pomiędzy wyróżnioną pozycją a wskazanym punktem.
Fragment kodu z wydruku 7.37 dokonuje obramowania płótna trójwymiarową ramką, dzięki
czemu komponent zyskuje wygląd podobny do przycisku:
Wydruk 7.37. Obramowanie płótna
int PenWidth = 2;
TColor Top = clBtnHighlight;
TColor Bottom = clBtnShadow;
Frame3D(Canvas, ClientRect, Top, Bottom, PenWidth);
Dzięki właściwości Canvas możliwe jest również wykorzystanie szerokiej gamy funkcji API,
operujących w oparciu o kontekst urządzenia. Kontekst ten dostępny jest pod właściwością
Handle płótna, może być również otrzymany za pomocą funkcji GetDC() na podstawie
uchwytu kontrolki; wykonanie każdej z poniższych instrukcji daje identyczny efekt:
HDC dc = SomeComponent >Canvas >Handle;
...
HDC dc = GetDC(SomeComponent >Handle);
Ilustracją wykorzystania płótna komponentu są trzy przykładowe projekty, znajdujące się w
katalogach PaintBox1, PaintBox2 i PaintBox3 na załączonej płycie CD-ROM. Używają
one komponentu TPaintBox, ponieważ jego właściwość Canvas została opublikowana; jako
że komponent ten w głównej mierze odpowiedzialny jest za wygląd formularza, rysowanie jego
zawartości powinno stanowić część scenariusza  odrysowywania okna w odpowiedzi na
komunikat WM_PAINT, powinno więc stanowić treść funkcji obsługującej zdarzenie OnPaint.
Pierwszy z projektów ilustruje rysowanie elipsy, będącej w istocie prostokątem o przesadnie
zaokrąglonych narożnikach; jego funkcję  odrysowującą przedstawia wydruk 7.38.
Wydruk 7.38. Rysowanie elipsy na płótnie komponentu
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TRect Rect = PaintBox1->ClientRect;
int nLeftRect, nTopRect, nRightRect, nBottomRect, nWidth, nHeight;
nLeftRect = Rect.Left;
nTopRect = Rect.Top;
nRightRect = Rect.Right;
nBottomRect = Rect.Bottom;
nWidth = Rect.Right - Rect.Left + 50;
nHeight = Rect.Bottom - Rect.Top;
if(RoundRect(
36
PaintBox1->Canvas->Handle, // uchwyt kontekstu urządzenia
nLeftRect, // współrzędne prostokąta
nTopRect, // ...
nRightRect, // ...
nBottomRect, // ...
nWidth, // osie elipsy określającej zaokrąglenie narożników
nHeight // ...
) == 0)
ShowMessage("Rysowanie nie udało się ...");
}
Pouczającym doświadczeniem może być zaobserwowanie skutków zmian wartości deklarowanych
zmiennych  zarówno tych określających współrzędne prostokąta (nLeftRect, nTopRect,
nRightRect, nBottomRect), jak i tych decydujących o wyglądzie zaokrąglonych narożników
(nWidth, nHeight); zerowa wartość tych ostatnich oznacza brak zaokrąglenia.
Wykorzystanie kontrolek graficznych
Do wielu czynników decydujących o atrakcyjności aplikacji dołączył w ostatnim dziesięcioleciu
również ich wygląd graficzny. Rozmaite programy komercyjne, shareware i freeware prześcigają
się w rozmaitych pomysłach graficznych, a powszechnie używane kontrolki, zadowalające się
niegdyś wyłącznie opisem tekstowym, wypierane są konsekwentnie przez swe odpowiedniki
graficzne  czego przykładem chociażby przyciski TSpeedButton i TBitBtn. Wychodząc
naprzeciw tym realiom, narzędzia do wizualnego projektowania aplikacji, jak Delphi i
C++Builder, oferują szereg klas ułatwiających zarządzanie bitmapami, ikonami, obrazkami typu
JPEG, GIF itp.
Kolejny z naszych przykładów ilustruje wszechobecną już w aplikacjach manierę symulowania
trójwymiarowego wyglądu kontrolek. Sugestywne wrażenie wypukłości lub wklęsłości prostokąta
powodowane jest w rzeczywistości odpowiednio dobranymi odcieniami ramki otaczającej ów
prostokąt, o czym można się łatwo przekonać, studiując kod zródłowy projektu. Bieżący stan
prostokąta (wypukłość  wklęsłość) określony jest przez prywatną zmienną IsUp; zmienna ta
przełączana jest przy każdym kliknięciu przycisku, który przy okazji dostosowuje swój tytuł do
bieżącej sytuacji:
Tu proszę wkleić rysunek AG-9-1.BMP.
Rysunek 7.1. Symulowanie trójwymiarowego wyglądu kontrolki
Wydruk 7.39. Przełączanie stanu prostokąta
void __fastcall TForm1::Button1Click(TObject *Sender)
{
IsUp = !IsUp;
Button1->Caption = (IsUp) ? "Wklęsły" : "Wypukły";
PaintBox1->Repaint();
}
37
Ostatnia z instrukcji wywołuje metodę odrysowującą zawartość prostokąta:
Wydruk 7.40. Rysowanie prostokąta
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TColor TopColor, BottomColor;
TRect Rect;
Rect = PaintBox1->ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
bit->Width = PaintBox1->Width;
bit->Height = PaintBox1->Height;
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(Rect);
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);
PaintBox1->Canvas->Draw(0, 0, bit);
delete bit;
}
Spoglądając na powyższy wydruk, nietrudno zauważyć jeszcze jedną tendencję typową dla
aplikacji graficznych. Mianowicie, aby zminimalizować migotanie poszczególnych elementów
graficznych, opatrująca je grafika tworzona jest najpierw na roboczej, niewidocznej dla
użytkownika bitmapie (w powyższym wydruku bitmapę taką wskazuje zmienna bit), a następnie
w szybki sposób kopiowana na płótno komponentu docelowego (w powyższym wydruku czyni to
przedostatnia z instrukcji).
Kolory ramki otaczającej prostokąt, rysowanej za pomocą funkcji API Frame3D(), określane są
za pomocą metody SwapColors(), która po prostu zapożycza je ze standardowego przycisku i
ustawia we właściwej kolejności stosownie do wartości zmiennej IsUp:
Wydruk 7.40. Określanie kolorów ramki
void __fastcall TForm1::SwapColors(TColor &Top, TColor &Bottom)
{
Top = (IsUp) ? clBtnHighlight : clBtnShadow;
Bottom = (IsUp) ? clBtnShadow : clBtnHighlight;
}
W rzeczywistości wygląd kontrolek graficznych w  prawdziwych aplikacjach jest nieco bardziej
złożony, zamiast bowiem zwykłych linii czy elips mamy tu do czynienia z kompletnymi
bitmapami, zazwyczaj przechowywanymi w odrębnych plikach lub zasobach (patrz rys. 7.2).
Komplikuje to ponownie czynność utrzymywania właściwego wyglądu kontrolek: zawartość
wspomnianych plików (zasobów) wczytywana jest do osobnych roboczych bitmap, te ostatnie
wkopiowywane są na  zbiorczą bitmapę roboczą, która dopiero kopiowana jest na płótno
komponentu. Ilustruje to trzeci z naszych projektów demonstracyjnych  funkcję obsługującą
zdarzenie OnPaint użytego w nim komponentu TPaintBox przedstawia wydruk 7.41.
38
Tu proszę wkleić rysunek AG-9-2.BMP
Rysunek 7.2. Wzbogacenie wyglądu kontrolki za pomocą zewnętrznej bitmapy
Wydruk 7.41. Wykorzystanie bitmap zewnętrznych
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TColor TopColor, BottomColor;
TRect Rect, gRect;
Rect = PaintBox1->ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
Graphics::TBitmap *bitFile = new Graphics::TBitmap;
bitFile->LoadFromFile("geom1b.bmp");
// rozmiar roboczej bitmapy równy jest rozmiarowi płótna komponentu
bit->Width = PaintBox1->Width;
bit->Height = PaintBox1->Height;
// wypełnij płótno bitmapy domyślnym kolorem pędzla
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(Rect);
// wycentruj zewnętrzną bitmapę (w poziomie i w pionie)
// na płótnie komponentu
gRect.Left = ((Rect.Right - Rect.Left) / 2) - (bitFile->Width / 2);
gRect.Top = ((Rect.Bottom - Rect.Top) / 2) - (bitFile->Height / 2);
// jeśli wklęsłość, to przesuń wewnętrzny prostokąt o 1 piksel
// w prawo i w dół
gRect.Top += (IsUp) ? 0 : 1;
gRect.Left += (IsUp) ? 0 : 1;
gRect.Right = bitFile->Width + gRect.Left;;
gRect.Bottom = bitFile->Height + gRect.Top;
// kopiuj na bitmapę zbiorczą, respektując kolor przezroczysty
bit->Canvas->BrushCopy(gRect, bitFile,
TRect(0,0,bitFile->Width, bitFile->Height), bitFile->TransparentColor);
// narysuj obrzeże
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);
// kopiuj zbiorczą bitmapę na płótno komponentu
BitBlt(PaintBox1->Canvas->Handle, 0, 0, PaintBox1->ClientWidth,
PaintBox1->ClientHeight, bit->Canvas->Handle, 0, 0, SRCCOPY);
delete bitFile; // usuń bitmapę wczytaną z pliku
delete bit; // usuń bitmapę zbiorczą
}
39
Reagowanie na zdarzenia pochodzące od myszy
Kontrolki graficzne konstruowane są zazwyczaj na bazie klasy TGraphicControl, nie są więc
kontrolkami okienkowymi, i nie posiadając uchwytu (Handle), nie mogą przyjmować skupienia.
Mimo to biblioteka VCL udostępnia mechanizmy umożliwiające reagowanie kontrolkom
graficznym na zdarzenia pochodzące od myszy.
Dobrze znany wszystkim projektantom przycisk TSpeedButton, gdy ustawić na true jego
właściwość Flat, pozostaje wkomponowany w tło, nie ukazując swego obrzeża; obrzeże to staje
się jednak natychmiast widoczne, gdy zatrzymać nad rzeczonym przyciskiem kursor myszy (rys.
7.3). Odpowiedzialnymi za ten efekt są dwa komunikaty Windows  CM_MOUSEENTER i
CM_MOUSELEAVE. Innym równie użytecznym komunikatem jest CM_ENABLEDCHANGED,
zmieniający status  dostępności kontrolki; jest on wysyłany do kontrolki każdorazowo, gdy
zmienia się jej właściwość Enabled:
procedure TControl.SetEnabled(Value: Boolean);
begin
if FEnabled <> Value then
begin
FEnabled := Value;
Perform(CM_ENABLEDCHANGED, 0, 0);
end;
end;
Tu proszę wkleić rysunek AG-9-3.BMP
Rysunek 7.3.  Płaski przycisk na formularzu
Wymienione komunikaty obsługiwane są przez dedykowane metody komponentu, co wynika z
deklaracji w jego pliku nagłówkowym3:
Wydruk 7.42. Deklaracja metod obsługujących komunikaty CM_MOUSEENTER,
CM_MOUSELEAVE i CM_ENABLEDCHANGED
...
MESSAGE void __fastcall CMMouseEnter(TMessage &Msg);
MESSAGE void __fastcall CMMouseLeave(TMessage &Msg);
MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg);
...
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)
MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)
MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)
3
W rzeczywistości fragment ten pochodzi nie z deklaracji klasy TSpeedButton, lecz z przykładowego
przycisku TExampleButton, którzy autorzy stworzyli (na bazie TGraphicControl) na użytek
niniejszego rozdziału  przyp. tłum.
40
END_MESSAGE_MAP(TGraphicControl)
Typowe zdarzenia związane z myszą  OnMouseUp, OnMouseDown, OnMouseOver itp. 
obsługiwane są przez metody klasy TControl zdefiniowane w jej sekcji protected. Można je
oczywiście przedefiniować, nie zapominając, by ich deklaracje pozostały w sekcji protected.
Ilustruje to wydruk 7.43.
Wydruk 7.43. Przedefiniowanie standardowej obsługi zdarzeń myszy
....
private:
TMouseEvent FOnMouseUp;
TMouseEvent FOnMouseDown;
TMouseMoveEvent FOnMouseMove;
protected:
DYNAMIC void __fastcall MouseDown
(TMouseButton Button, TShiftState Shift, int X, int Y);
DYNAMIC void __fastcall MouseMove
(TShiftState Shift, int X, int Y);
DYNAMIC void __fastcall MouseUp
(TMouseButton Button, TShiftState Shift, int X, int Y);
__published:
__property TMouseEvent OnMouseUp = {read=FOnMouseUp, write=FOnMouseUp};
__property TMouseEvent OnMouseDown =
{read=FOnMouseDown, write=FOnMouseDown};
__property TMouseMoveEvent OnMouseMove =
{read=FOnMouseMove, write=FOnMouseMove};
....
Przykład zastosowania
Ilustracją przedstawionych informacji jest przykładowy komponent TExampleButton, którego
moduł zródłowy znajduje się na załączonej płycie CD-ROM (pliki ExampleButton.h i
ExampleButton.cpp). Jest on daleki od kompletności i jako taki stanowić może wdzięczny
materiał do eksperymentowania, niemniej jednak i tak w obecnej postaci nadaje się do
zainstalowania w palecie komponentów. Treść wspomnianych plików zródłowych prezentujemy
na dwóch poniższych wydrukach.
Wydruk 7.44. Plik nagłówkowy ExampleButton.h
// TExampleButton
// By: Sean Rock, rev. by A.Grażyński
//---------------------------------------------------------------------------
41
#ifndef ExampleButtonH
#define ExampleButtonH
//---------------------------------------------------------------------------
#include
#include
#include
#include
//---------------------------------------------------------------------------
enum TExButtonState {esUp, esDown, esFlat, esDisabled};
class PACKAGE TExampleButton : public TGraphicControl
{
private:
Graphics::TBitmap *FGlyph;
AnsiString FCaption;
TImageList *FImage;
TExButtonState FState;
bool FMouseInControl;
TNotifyEvent FOnClick;
void __fastcall SetGlyph(Graphics::TBitmap *Value);
void __fastcall SetCaption(AnsiString Value);
void __fastcall BeforeDestruction(void);
void __fastcall SwapColors(TColor &Top, TColor &Bottom);
void __fastcall CalcGlyphLayout(TRect &r);
void __fastcall CalcTextLayout(TRect &r);
MESSAGE void __fastcall CMMouseEnter(TMessage &Msg);
MESSAGE void __fastcall CMMouseLeave(TMessage &Msg);
MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg);
protected:
void __fastcall Paint(void);
DYNAMIC void __fastcall MouseDown(TMouseButton Button, TShiftState Shift,
int X, int Y);
DYNAMIC void __fastcall MouseUp(TMouseButton Button, TShiftState Shift,
int X, int Y);
public:
__fastcall TExampleButton(TComponent* Owner);
__published:
__property AnsiString Caption = {read=FCaption, write=SetCaption};
__property Graphics::TBitmap * Glyph = {read=FGlyph, write=SetGlyph};
__property TNotifyEvent OnClick = {read=FOnClick, write=FOnClick};
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)
MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)
MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)
END_MESSAGE_MAP(TGraphicControl)
};
//---------------------------------------------------------------------------
#endif
Wydruk 7.45. Plik ExampleButton.cpp
// ExampleButton.cpp
// By: Sean Rock, rev. by A.Grażyński
42
//---------------------------------------------------------------------------
#include
#pragma hdrstop
#include "ExampleButton.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TExampleButton *)
{
new TExampleButton(NULL);
}
//---------------------------------------------------------------------------
__fastcall TExampleButton::TExampleButton(TComponent* Owner)
: TGraphicControl(Owner)
{
SetBounds(0,0,50,50);
ControlStyle = ControlStyle << csReplicatable;
FState = esFlat;
}
//---------------------------------------------------------------------------
namespace Examplebutton
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TExampleButton)};
RegisterComponents("Samples", classes, 0);
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMMouseEnter(TMessage &Msg)
{
if(Enabled)
{
FState = esUp;
FMouseInControl = true;
Invalidate();
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMMouseLeave(TMessage &Msg)
{
if(Enabled)
{
FState = esFlat;
FMouseInControl = false;
Invalidate();
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMEnabledChanged(TMessage &Msg)
{
FState = (Enabled) ? esFlat : esDisabled;
Invalidate();
}
// ---------------------------------------------------------------------------
43
void __fastcall TExampleButton::MouseDown(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esDown;
Invalidate();
}
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esUp;
Invalidate();
if(FOnClick)
FOnClick(this);
}
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SetGlyph(Graphics::TBitmap * Value)
{
if(Value == NULL)
return;
if(!FGlyph)
FGlyph = new Graphics::TBitmap;
FGlyph->Assign(Value);
Invalidate();
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SetCaption(AnsiString Value)
{
FCaption = Value;
Invalidate();
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SwapColors(TColor &Top, TColor &Bottom)
{
if(ComponentState.Contains(csDesigning))
{
FState = esUp;
}
Top = (FState == esUp) ? clBtnHighlight : clBtnShadow;
Bottom = (FState == esDown) ? clBtnHighlight : clBtnShadow;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::BeforeDestruction(void)
{
44
if(FImage)
delete FImage;
if(FGlyph)
delete FGlyph;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::Paint(void)
{
TRect cRect, tRect, gRect;
TColor TopColor, BottomColor;
Canvas->Brush->Color = clBtnFace;
Canvas->FillRect(ClientRect);
cRect = ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
bit->Width = ClientWidth;
bit->Height = ClientHeight;
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(cRect);
if(FGlyph)
if(!FGlyph->Empty)
{
CalcGlyphLayout(gRect);
bit->Canvas->BrushCopy(gRect, FGlyph,
Rect(0,0,FGlyph->Width,FGlyph->Height), FGlyph->TransparentColor);
}
if(!FCaption.IsEmpty())
{
CalcTextLayout(tRect);
bit->Canvas->TextRect(tRect, tRect.Left,tRect.Top, FCaption);
}
if(FState == esUp || FState == esDown)
{
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, cRect, TopColor, BottomColor, 1);
}
BitBlt(Canvas->Handle, 0, 0, ClientWidth, ClientHeight,
bit->Canvas->Handle, 0, 0, SRCCOPY);
delete bit;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CalcGlyphLayout(TRect &r)
{
int TotalHeight=0;
int TextHeight=0;
if(!FCaption.IsEmpty())
TextHeight = Canvas->TextHeight(FCaption);
// poniższa wielkość marginesu, ustalona arbitralnie na 5 pikseli,
// powinna być przedmiotem odrębnej właściwości
TotalHeight = FGlyph->Height + TextHeight + 5;
45
r = Rect((ClientWidth/2)-(FGlyph->Width/2),
((ClientHeight/2)-(TotalHeight/2)), FGlyph->Width +
(ClientWidth/2)-(FGlyph->Width/2), FGlyph->Height +
((ClientHeight/2)-(TotalHeight/2)));
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CalcTextLayout(TRect &r)
{
int TotalHeight=0;
int TextHeight=0;
int TextWidth=0;
TRect temp;
if(FGlyph)
TotalHeight = FGlyph->Height;
TextHeight = Canvas->TextHeight(FCaption);
TextWidth = Canvas->TextWidth(FCaption);
TotalHeight += TextHeight + 5;
temp.Left = 0;
temp.Top = (ClientHeight/2)-(TotalHeight/2);
temp.Bottom = temp.Top + TotalHeight;
temp.Right = ClientWidth;
r = Rect(((ClientWidth/2) - (TextWidth/2)), temp.Bottom-TextHeight,
((ClientWidth/2)-(TextWidth/2))+TextWidth, temp.Bottom);
}
Wśród opublikowanych zdarzeń znajduje się tu jedynie OnClick, chociaż w rzeczywistym
komponencie listę tę należałoby raczej poszerzyć o zdarzenia OnMouseUp, OnMouseDown i
OnMouseMove. Podobnie ma się sprawa z innymi właściwościami  do opublikowanych
Caption i Glyph prawdopodobnie powinna dołączyć także właściwość Font, reprezentująca
czcionkę, którą wypisany zostanie tytuł przycisku. Użyteczne może się również okazać
przechwycenie komunikatu CM_FONTCHANGED w celu dostosowania wzajemnego położenia
tytułu i obrazka określonego przez właściwość Glyph. Obecnie przyjęto sztywną,
pięciopikselową odległość pomiędzy nimi  tę sztywną wartość bardziej elegancko będzie jednak
powierzyć odrębnej właściwości.
Zwróćmy także uwagę na metodę SetGlyph(), będącą metodą dostępową właściwości Glyph.
Aatwo zauważyć, iż metoda ta nieczuła jest na przekazanie wartości NULL, co powoduje, iż raz
przypisanego komponentowi obrazka nie sposób już (na etapie projektowania) zlikwidować 
można to zrobić jedynie poprzez usunięcie komponentu z formularza i zastąpienie go nowym,
pobranym z palety.
USTERKA (1)
Opisywany komponent (TExampleButton) ma pewną usterkę, przejawiającą się również i w
niektórych innych komponentach  przypisanie właściwości Glyph jakiejś bitmapy (a więc
46
zmiana jej domyślnej wartości NULL) powoduje przy uruchomieniu aplikacji generowanie
wyjątku związanego z niemożnością odczytu tej właściwości ze strumienia.
Ostatnim elementem wartym krótkiego komentarza jest boolowska zmienna
FMouseInControl. Zmienna ta używana jest do określenia, czy kursor myszy znajduje się
aktualnie nad kontrolką; za utrzymywanie jej zgodności ze stanem faktycznym odpowiedzialne są
metody CMMouseEnter() i CMMouseLeave(). Znajomość położenia kursora jest o tyle
istotna, iż w pewnych sytuacjach kontrolka może wciąż reagować na zdarzenia myszy, mimo iż
kursor znajduje się już poza nią  będzie tak np. w przypadku umieszczenia kursora nad kontrolką,
a następnie przesunięcie go poza kontrolkę przy naciśniętym i trzymanym lewym przycisku.
Puszczenie przycisku spowoduje wywołanie metody MouseUp  jeżeli wartość zmiennej
FMouseInControl równa będzie false, metoda ta nie wykona jednak żadnej akcji:
void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esUp;
Invalidate();
if(FOnClick)
FOnClick(this);
}
}
}
Obrzeże przycisku  jeżeli oczywiście jest widoczne  rysowane jest za pomocą funkcji
Frame3D(). Można w tym celu użyć nieco bardziej zaawansowanej funkcji API o nazwie
DrawButtonFace()  jej deklaracja, znajdująca się w pliku nagłówkowym buttons.hpp,
jest następująca:
Wydruk 7.46. Deklaracja funkcji DrawButtonFace()
TRect __fastcall DrawButtonFace
(Graphics::TCanvas* Canvas, const Windows::TRect &Client,
int BevelWidth, TButtonStyle Style,
bool IsRounded, bool IsDown, bool IsFocused);
Funkcja ta rysuje  czoło przycisku o wielkości określonej przez parametr Client na płótnie
wskazanym przez parametr Canvas. Niektóre parametry istotne są tylko w pewnych
okolicznościach  na przykład parametry BevelWidth, IsRounded i IsFocused mają
znaczenie tylko wtedy, gdy styl przycisku (Style) równy jest bsWin31. Kod zródłowy funkcji
(w Object Pascalu) prezentujemy na wydruku 7.47  znajduje się on w pliku buttons.pas:
47
Wydruk 7.47. Implementacja funkcji DrawButtonFace()
function DrawButtonFace(Canvas: TCanvas; const Client: TRect;
BevelWidth: Integer; Style: TButtonStyle; IsRounded, IsDown,
IsFocused: Boolean): TRect;
var
NewStyle: Boolean;
R: TRect;
DC: THandle;
begin
NewStyle :=
((Style = bsAutoDetect) and NewStyleControls) or (Style = bsNew);
R := Client;
with Canvas do
begin
if NewStyle then
begin
Brush.Color := clBtnFace;
Brush.Style := bsSolid;
DC := Canvas.Handle;
if IsDown then
begin { DrawEdge jest szybsza niż Polyline }
DrawEdge(DC, R, BDR_SUNKENINNER, BF_TOPLEFT); { czarny }
DrawEdge(DC, R, BDR_SUNKENOUTER, BF_BOTTOMRIGHT); { btnhilite }
Dec(R.Bottom);
Dec(R.Right);
Inc(R.Top);
Inc(R.Left);
DrawEdge(DC, R, BDR_SUNKENOUTER, BF_TOPLEFT or BF_MIDDLE); {btnshadow}
end
else
begin
DrawEdge(DC, R, BDR_RAISEDOUTER, BF_BOTTOMRIGHT); { czarny }
Dec(R.Bottom);
Dec(R.Right);
DrawEdge(DC, R, BDR_RAISEDINNER, BF_TOPLEFT); {btnhilite}
Inc(R.Top);
Inc(R.Left);
DrawEdge(DC, R, BDR_RAISEDINNER, BF_BOTTOMRIGHT or BF_MIDDLE);
{btnshadow}
end;
end
else
begin
Pen.Color := clWindowFrame;
Brush.Color := clBtnFace;
Brush.Style := bsSolid;
Rectangle(R.Left, R.Top, R.Right, R.Bottom);
{zaokrąglenie narożników  dotyczy tylko przycisków w stylu Windows 3.1}
if IsRounded then
begin
Pixels[R.Left, R.Top] := clBtnFace;
Pixels[R.Left, R.Bottom - 1] := clBtnFace;
Pixels[R.Right - 1, R.Top] := clBtnFace;
Pixels[R.Right - 1, R.Bottom - 1] := clBtnFace;
48
end;
if IsFocused then
begin
InflateRect(R, -1, -1);
Brush.Style := bsClear;
Rectangle(R.Left, R.Top, R.Right, R.Bottom);
end;
InflateRect(R, -1, -1);
if not IsDown then
Frame3D(Canvas, R, clBtnHighlight, clBtnShadow, BevelWidth)
else
begin
Pen.Color := clBtnShadow;
PolyLine([Point(R.Left, R.Bottom - 1), Point(R.Left, R.Top),
Point(R.Right, R.Top)]);
end;
end;
end;
Result := Rect(Client.Left + 1, Client.Top + 1,
Client.Right - 2, Client.Bottom - 2);
if IsDown then OffsetRect(Result, 1, 1);
end;
Rozbudowa kontrolek okienkowych
Komponenty okienkowe  te wywodzące się z klasy TWinControl  stanowią reprezentację
standardowych kontrolek Windows na gruncie biblioteki VCL. Budowanie na ich bazie
komponentów pochodnych ma na celu nie tyle zmianę ich wyglądu (jak w przypadku
komponentów  graficznych ), lecz raczej wzbogacanie i modyfikację ich standardowego
zachowania; wiele aspektów owego zachowania określonych jest przez chronione (protected)
metody komponentów.
W tym podrozdziale zaprezentujemy rozbudowę komponentu TFileListBox umieszczonego w
palecie na stronie Win31 i przedstawiającego listę nazw plików danego katalogu. Do spełnianych
obecnie przez niego funkcji dodamy następujące elementy:
" przypisanie każdemu plikowi odpowiedniej ikony, zgodnie ze standardami Windows;
" uruchomienie pliku lub otwarcie dokumentu w rezultacie dwukrotnego kliknięcia
reprezentującej go pozycji;
" umożliwienie bardziej selektywnego doboru zestawu plików wyświetlanych w liście;
" wybranie elementu listy w wyniku kliknięcia prawym przyciskiem;
" przewijanie listy w poziomie, jeżeli któryś z elementów nie mieści się w niej w całości.
Zachowamy jednocześnie bardzo istotną cechę oryginalnego komponentu, mianowicie
49
" połączenie z komponentem TDirectoryListBox za pośrednictwem jego właściwości
FileList.
Rysunek 7.4 ukazuje różnicę pomiędzy obydwiema kontrolkami  oryginalną (TFileListBox)
i rozbudowaną (TSHFileListBox)  ukazującymi ten sam fragment katalogu.
50
Rysunek 7.4. Kontrolki TFileListBox i TSHFileListBox
51
Powyższy rysunek znajduje się w pliku AG-9-4.BMP. Otoczyłem rysunek separatorami strony
tylko po to, by było wiadome, gdzie mają wskazywać strzałki.
Na początku należy się oczywiście zastanowić nad wyborem klasy bazowej naszego komponentu.
Biblioteka VCL zawiera szeroką gamę komponentów o nazwach rozpoczynających się od
TCustom..., które to komponenty przeznaczone są właśnie do pełnienia  materiału wyjściowego
dla nowo tworzonych  prawdziwych komponentów. Bezpośrednim przodkiem komponentu
TFileListBox jest TCustomListBox, tego ostatniego niestety nie da się użyć na nasze
potrzeby ze względu na postulat wyrażony w ostatnim punkcie prezentowanej przed chwilą listy 
właściwość TDirectoryListBox:: FileList stanowi bowiem wskazanie na komponent
typu TFileListBox, a więc inspektor obiektów nie umożliwiłby skojarzenia z nią komponentu
wyprowadzonego z innej klasy; zmuszeni jesteśmy więc pozostać przy klasie TFileListBox
jako klasie bazowej.
Skoro wybraliśmy już klasę bazową, przystąpmy do jej uzupełniania o niezbędne właściwości,
metody i zdarzenia. Rozpoczniemy od  uruchamiania wybranego elementu lub skojarzonej z nim
aplikacji w wyniku dwukrotnego kliknięcia. Dwukrotne kliknięcie w obszarze listy powoduje
najpierw wybranie ( podświetlenie ) elementu znajdującego się aktualnie pod kursorem, a
następnie wygenerowanie wywołanie metody DblClick listy jako całości.
Metoda ta zdefiniowana jest w klasie TControl w postaci niżej podanej i w klasie
TFileListBox pozostaje niezmieniona:
procedure TControl.DblClick;
begin
if Assigned(FOnDblClick) then FOnDblClick(Self);
end;
Nasz komponent rozszerzony zostanie o możliwość uprzedniego  uruchomienia wybranego
elementu (dokładniej  wszystkich wybranych elementów, bowiem lista umożliwiać może wybór
wielokrotny)  akcja ta wykonywana będzie zależnie od wartości (prywatnej) zmiennej
boolowskiej FCanLaunch. Oto treść zmodyfikowanej funkcji, obsługującej zdarzenie
dwukrotnego kliknięcia:
Wydruk 7.48. Obsługa zdarzenia OnDblClick
void __fastcall TSHFileListBox::DblClick(void)
{
if(FCanLaunch) // czy skojarzenie aktywne?
{
int ii=0;
// obsłuż podświetlone elementy listy
for(ii=0; ii < Items->Count; ii++)
{
if(Selected[ii])
{
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);
}
52
}
}
// wywołaj funkcję obsługi zdarzenia OnDblCLick
if(FOnDblClick)
FOnDblClick(this);
}
Uruchomienia aplikacji skojarzonej z danym elementem listy (lub aplikacji przez ten element
reprezentowanej) dokonuje funkcja API o nazwie ShellExecute(). Pierwszy parametr jej
wywołania jest uchwytem macierzystego okna aplikacji (tu: formularza głównego), drugi określa
rodzaj akcji do wykonania (tu: otwarcie), trzeci natomiast jest łańcuchem ASCIIZ, zawierającym
nazwę pliku.
Po obsłużeniu wszystkich  podświetlonych elementów wywołana zostaje ta funkcja, którą w
inspektorze obiektów przyporządkowano zdarzeniu OnDblClick; właściwość reprezentująca to
zdarzenie bazuje bowiem na prywatnym polu FOnDblClick
__property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick}
wykorzystywanym w końcówce cytowanej funkcji.
USTERKA (2)
Niestety, to wszystko to tylko teoria. bo autor przeoczył bardzo istotną okoliczność: jeżeli plik
reprezentowany przez daną pozycję należy do zarejestrowanych typów plików, nie jest
wyświetlane jego rozszerzenie, a po dwukrotnym jego kliknięciu do funkcji
ShellExecute()przekazywana jest nazwa bez rozszerzenia i cała idea bierze w łeb, bo
funkcja ta wybiera skojarzoną aplikację właśnie na podstawie rozszerzenia. TRZEBA TO
ZMIENIĆ. BO CZYTELNICY SI NA TO NIE ZGODZ. Niestety, ja nie potrafię.
AG
Kolejne rozszerzenie naszego komponentu to wybranie pozycji listy w wyniku kliknięcia prawym
przyciskiem. Funkcja ta uzależniona jest od właściwości RightBtnClick, bazującej na
prywatnym polu FRightBtnSelect
__property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel,
default=true};
zaś jej zaimplementowanie opiera się na przechwyceniu zdarzenia OnMouseUp generowanego w
momencie puszczenia prawego przycisku:
Wydruk 7.49. Obsługa zdarzenia OnMouseUp
53
void __fastcall TSHFileListBox::MouseUp(TMouseButton Button,
TShiftState Shift, int X, int Y)
{
// interesuje nas tylko prawy klawisz
if(!FRightBtnSel)
return;
TPoint ItemPos = Point(X,Y);
// czy pod kursorem znajduje się konkretna pozycja ?
int Index = ItemAtPos(ItemPos, true);
// nie, nie zaznaczaj żadnej pozycji
if(Index == -1)
return;
// zaznacz pozycję pod kursorem
Perform(LB_SETCURSEL, (WPARAM)Index, 0);
}
W momencie puszczenia prawego klawisza sprawdza się, czy kursor myszy znajduje się nad
którąś z pozycji listy  metoda ItemAtPos() próbuje skojarzyć punkt o wskazanych
współrzędnych (tu: wierzchołek kursora) z konkretną pozycją listy, zwracając indeks tej pozycji;
jeżeli skojarzenie takie nie jest możliwe (we wskazanym miejscu nie ma żadnej pozycji), metoda
zwraca wartość  1. W przypadku udanego skojarzenia symulowane jest wystąpienie komunikatu
LB_SETCURSEL, nakazującego zaznaczenie pozycji o wskazanym indeksie.
USTERKA (2)
Niestety, to również nie jest dokładnie tak: zaznaczanie prawym klawiszem nie działa, jeżeli
ustawiony jest tryb wielokrotnego zaznaczania (MultiSelect=true).
Komponent TFileListBox niekoniecznie musi ukazywać wszystkie pliki danego katalogu  za
pomocą właściwości Mask można mianowicie ograniczyć zestaw widocznych plików tylko do
tych, których nazwa zgodna jest z wyspecyfikowaną maską (w zwyczajowym rozumieniu znaków
blankietowych  * oraz  ? ). Proces  filtrowania plików można jednak uczynić bardziej
selektywnym  i to właśnie jest istotą kolejnego rozszerzenia. Każdorazowo, gdy nazwa pliku 
po stwierdzeniu jej zgodności z maską  ma zostać dodana do listy, generowane jest zdarzenie
OnAddItem o typie określonym następująco:
typedef void __fastcall (__closure *TAddItemEvent)
(TObject *Sender, AnsiString Item, bool &CanAdd);
Pierwszy parametr (Sender) stanowi oczywiście wskazanie na listę, drugi (Item) jest
łańcuchem podlegającym kwalifikowaniu, trzeci natomiast określa wynik tego kwalifikowania 
jeżeli jego domyślna wartość true zostanie (w ramach funkcji obsługi) zamieniona na false,
badana pozycja nie zostanie dodana do listy.
54
USTERKA (3)
Kolejna usterka komponentu polega na tym, iż w przypadku zmiany domyślnej wartości maski
*.* w uruchomionej aplikacji generowany jest wyjątek w związku z niemożnością odczytu tej
właściwości ze strumienia.
Następne rozszerzenie naszego komponentu polega na opatrzeniu wyświetlanych nazw plików
stosownymi ikonami  na takiej samej zasadzie, jak czyni to Eksplorator Windows. Oto
najistotniejszy fragment kodu związany z tym procesem:
Wydruk 7.50. Odczytanie listy ikon odpowiadających poszczególnym plikom
void __fastcall TSHFileListBox::GetSysImages(void)
{
SHFILEINFO shfi;
DWORD iHnd;
if(!FImages)
{
FImages = new TImageList(this);
FImages->ShareImages = true;
FImages->Height = 16;
FImages->Width = 16;
iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX |
SHGFI_SHELLICONSIZE | SHGFI_SMALLICON);
if(iHnd != 0)
FImages->Handle = iHnd;
}
}
Funkcja SHGetFileInfo() dokonuje odczytu niezbędnej informacji systemowej związanej z
ikonami plików. Struktura danych niosąca tę informację, identyfikowana przez uchwyt zwracany
przez funkcję SHGetFileInfo(), reprezentowana jest na gruncie VCL przez obiekt
TImageList, zaś wspomniany uchwyt ukrywa się pod jego właściwością Handle. Z
właściwością tą związana jest inna istotna właściwość  ShareImages: ustawienie jej na true
powoduje, iż obiekt TImageList podczas swej destrukcji nie zwalnia uchwytu kryjącego
się pod właściwością Handle  analizując wydruk 7.50 nietrudno zauważyć, iż uchwyt ten
utworzony został niezależnie od tegoż obiektu, stanowiącego tylko swego rodzaju  obudowę .
Generalnie rzecz biorąc, właścicielem uchwytu zwracanego przez SHGetFileInfo() jest
system operacyjny  zwolnienie tegoż uchwytu spowodowałoby nieodwracalną (aż do ponownego
załadowania systemu) utratę wszelkich ikon opatrujących obiekty Eksploratora, opcje menu itp.
Szczegółowy opis funkcji SHGetFileInfo() znajduje się w systemie pomocy Windows
SDK.
Metoda AddItem(), dodająca do wyświetlanej listy kolejnej nazwy wraz z odpowiadającą jej
ikoną, skonstruowana została w oparciu o interfejs COM (ang. Component Object Model); jej kod
zródłowy przedstawia wydruk 7.51, lecz szczegółowa analiza użytych tu mechanizmów wykracza
55
poza ramy tego rozdziału. Element reprezentujący systemową strukturę, zawierającą informację o
dodawanej nazwie pliku, przekazywany jest jako jedyny parametr wywołania (definicja typu
LPITEMIDLIST dostępna jest w systemie pomocy  Windows SDK pod hasłem  Item
Identifiers and Identifier Lists ), zaś wynikiem funkcji jest szerokość (w pikselach) tekstu
wypisującego tę nazwę (jeżeli nazwa nie zostanie dodana do listy, funkcja zwraca wartość 0).
Wydruk 7.51. Dodawanie kolejnej nazwy do listy
int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)
{
SHFILEINFO shfi;
int Index;
SHGetFileInfo(
(char*)pidl,
0,
&shfi,
sizeof(shfi),
SHGFI_PIDL | SHGFI_SYSICONINDEX | SHGFI_SMALLICON |
SHGFI_DISPLAYNAME | SHGFI_USEFILEATTRIBUTES
);
// zweryfikuj nazwę pod kątem dodania jej do listy,
// generując zdarzenie OnAddItem
bool FCanAdd = true;
if(FOnAddItem)
FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);
if(FCanAdd)
{
TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);
Index = Items->AddObject(AnsiString(shfi.szDisplayName),
(TObject*)ShellInfo);
// zwróć szerokość tekstu reprezentującego nazwę
return Canvas->TextWidth(Items->Strings[Index]);
}
// zwróć zero jako sygnał, iż nazwa nie została dodana do listy
return 0;
}
Wytłuszczony fragment to opisana przed chwilą weryfikacja nazwy za pomocą generowanego
zdarzenia OnAddItem. Jeżeli nazwa zakwalifikowana zostanie do dodania jej do listy, wraz z nią
dodawany jest do tejże listy (jako element właściwości Objects[]) obiekt klasy
TShellFileListItem, zawierający m.in. indeks ikony (w liście utworzonej na wydruku 7.50)
odpowiadającej tej nazwie; umożliwi to pózniejsze opatrywanie wypisywanych nazw stosownymi
ikonami.
Obiekt TShellFileListItem zawiera także kopię parametru wywołania metody
AddItem(). Kopia ta może zostać wykorzystana do przyszłych rozszerzeń komponentu, np. dla
wyświetlenia menu kontekstowego związanego z daną nazwą.
56
UWAGA ODNOŚNIE USTERKI (2)
Może właśnie za pomocą wspomnianej kopii uda się naprawić usterkę nr 2; kopia ta zawiera
prawdopodobnie pełną nazwę wraz z rozszerzeniem, co można wnioskować po takim fragmencie
wydruku 9.51 (w oryginale  str 374):
SHGetFileInfo(
(char*)pidl,
.....
);
i wobec tego być może w metodzie DblCLick() fragment:
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(),
0,0,SW_SHOWDEFAULT);
można by zastąpić takim fragmentem:
TShellFileListItem *thisSHI =
reinterpret cast(Items >Objects[ii]);
LPITEMIDLIST thisPIDL = thisSHI->pidl;
ShellExecute(Handle, "open", (char*)thisPIDL, 0, 0,
SW_SHOWDEFAULT);
Proszę znawców C++Buildera o zastanowienie się nad tym.
Aby przechowywane pod właściwością Objects[] pomocnicze obiekty
TShellFileListItem zostały zwolnione w momencie destrukcji listy, konieczne jest
przedefiniowanie jej metody DeleteString():
Wydruk 7.52. Zwalnianie pozycji listy
void __fastcall TSHFileListBox::DeleteString(int Index)
{
57
// pobierz wskaznik do obiektu pomocniczego i zwolnij ten obiekt
TShellFileListItem *ShellItem = reinterpret_cast
(Items->Objects[Index]);
delete ShellItem;
ShellItem = NULL;
// usuń tekst związany z pozycją
Items->Delete(Index);
}
Wspomnieliśmy przed chwilą, iż wynik zwracany przez metodę AddItem() równy jest
szerokości dodawanej do listy pozycji (lub zeru, gdy pozycja nie zostanie dodana). Fakt ten
wykorzystywany jest w metodzie ReadFileNames() odpowiedzialnej za skompletowanie listy
 po wykonaniu poniższego fragmentu tej metody zmienna l zawiera szerokość najszerszej
pozycji, co wykorzystywane jest pózniej do rozstrzygnięcia, czy listę należy wyposażyć w
poziomy pasek przewijania:
Wydruk 7.53. Obliczanie szerokości zajmowanej przez pozycje listy
ULONG celt = 1;
ULONG Fetched = 0;
ppenumIDList->Next(celt, &rgelt, &Fetched);
hExtent = 0;
while(Fetched > 0)
{
// dodaj pozycję do listy
int l = AddItem(rgelt);
if(l > hExtent)
{
hExtent = l;
}
ppenumIDList->Next(celt, &rgelt, &Fetched);
}
Obliczona szerokość zwiększana jest o dwupikselowy margines, a jeżeli właściwość
ShowGlyphs ma wartość true  również o 18 pikseli na ikonę i jej odstęp od tekstu. Tak
obliczona wartość przekazywana jest do listy pod postacią komunikatu
LB_SETHORIZONTALEXTENT  jeżeli będzie ona większa od szerokości okna listy, u jej dolnej
krawędzi automatycznie pojawi się pasek przewijania:
Wydruk 7.54. Wyposażenie listy w poziomy pasek przewijania
void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)
{
// dodaj minimalny margines
he += 2;
// jeżeli mają być widoczne ikony, dodaj 16 pikseli na ikonę
// i 2 piksele na jej odstęp od tekstu
if(ShowGlyphs)
he += 18;
58
// wyślij komunikat, który w razie potrzeby ustanowi
// poziomy pasek przewijania
Perform(LB_SETHORIZONTALEXTENT, he, 0);
}
Na tym kończy się repertuar nowych możliwości naszego komponentu. Poniżej przedstawiamy
kompletny kod jego modułu zródłowego  znajduje się on również na dołączonej do książki płycie
CD-ROM.
W związku z opisanymi usterkami poniższe listingi mogą ulec zmianom, należy je więc
zweryfikować przed składem
Wydruk 7.55. SHFileListBox.h  plik nagłówkowy komponentu TSHFileListBox
//---------------------------------------------------------------------------
#ifndef SHFileListBoxH
#define SHFileListBoxH
//---------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
//---------------------------------------------------------------------------
class TShellFileListItem : public TObject
{
private:
LPITEMIDLIST Fpidl;
int FImageIndex;
public:
__fastcall TShellFileListItem(LPITEMIDLIST lpidl, int Index);
__fastcall ~TShellFileListItem(void);
__property LPITEMIDLIST pidl = {read=Fpidl};
__property int ImageIndex = {read=FImageIndex};
};
typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString
Item, bool &CanAdd);
class PACKAGE TSHFileListBox : public TFileListBox
{
private:
TImageList *FImages;
TNotifyEvent FOnDblClick;
bool FCanLaunch;
bool FRightBtnSel;
TAddItemEvent FOnAddItem;
int __fastcall AddItem(LPITEMIDLIST pidl);
void __fastcall GetSysImages(void);
protected:
59
DYNAMIC void __fastcall DblClick(void);
void __fastcall ReadFileNames(void);
DYNAMIC void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int
X, int Y);
void __fastcall DrawItem(int Index, const TRect &Rect, TOwnerDrawState
State);
void __fastcall DoHorizontalScrollBar(int he);
DYNAMIC void __fastcall DeleteString(int Index);
public:
__fastcall TSHFileListBox(TComponent* Owner);
__fastcall ~TSHFileListBox(void);
__published:
__property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch,
default=true};
__property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel,
default=true};
__property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick};
__property TAddItemEvent OnAddItem = {read=FOnAddItem, write=FOnAddItem};
};
extern "C" LPITEMIDLIST CopyPIDL(LPITEMIDLIST lpidl);
extern "C" unsigned short GetPIDLSize(LPITEMIDLIST lpidl);
extern "C" LPITEMIDLIST CreatePIDL(unsigned short size);
extern "C" LPITEMIDLIST GetNextItem(LPITEMIDLIST pidl);
//---------------------------------------------------------------------------
#endif
Wydruk 7.56. SHFileListBox.cpp  plik implementacyjny komponentu TSHFileListBox
// TSHFileListbox
// By: Sean Rock
// roks@bigfoot.com
// http://www.theblack.co.uk
// last modified: 11 Aug 00
//---------------------------------------------------------------------------
//#include
#pragma hdrstop
#include "SHFileListBox.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
__fastcall TShellFileListItem::TShellFileListItem(LPITEMIDLIST lpidl, int
Index)
: TObject()
{
// zachowaj kopię elementu systemowego  pidl reprezentującego plik...
Fpidl = CopyPIDL(lpidl);
// .. oraz indeks ikony przeznaczonej dla pliku
FImageIndex = Index;
}
//----------------------------------------------------------------------------
__fastcall TShellFileListItem::~TShellFileListItem(void)
{
LPMALLOC lpMalloc=NULL;
if(SUCCEEDED(SHGetMalloc(&lpMalloc)))
{
// zwolnij pamięć zajętą przez element systemowy  pidl
60
// reprezentujący plik
lpMalloc->Free(Fpidl);
lpMalloc->Release();
}
}
//---------------------------------------------------------------------------
__fastcall TSHFileListBox::TSHFileListBox(TComponent* Owner)
: TFileListBox(Owner)
{
ItemHeight = 18;
ShowGlyphs = true;
FCanLaunch = true;
FRightBtnSel = true;
}
//---------------------------------------------------------------------------
__fastcall TSHFileListBox::~TSHFileListBox(void)
{
// zwolnij listę ikon
if(FImages)
delete FImages;
FImages = NULL;
}
//----------------------------------------------------------------------------
void __fastcall TSHFileListBox::DeleteString(int Index)
{
// niniejsza metoda wywoływana jest w odpowiedzi na komunikat
// LB_DELETESTRING. Zwalnia ona łańcuch zawierający nazwę pliku oraz
// towarzyszący mu obiekt pomocniczy  pidl
TShellFileListItem *ShellItem = reinterpret_cast
(Items->Objects[Index]);
delete ShellItem;
ShellItem = NULL;
Items->Delete(Index);
}
//----------------------------------------------------------------------------
namespace Shfilelistbox
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TSHFileListBox)};
RegisterComponents("Samples", classes, 0);
}
}
//---------------------------------------------------------------------------
void __fastcall TSHFileListBox::ReadFileNames(void)
{
LPMALLOC g_pMalloc;
LPSHELLFOLDER pisf;
LPSHELLFOLDER sfChild;
LPITEMIDLIST pidlDirectory;
LPITEMIDLIST rgelt;
LPENUMIDLIST ppenumIDList;
int hExtent;
try
61
{
try
{
if(HandleAllocated())
{
GetSysImages();
// zablokuj uaktualnianie ekranu
Items->BeginUpdate();
// usuń całą zawartość listy
Items->Clear();
// pobierz wskaznik systemowego alokatora pamięci
if(SHGetMalloc(&g_pMalloc) != NOERROR)
{
return;
}
// uzyskaj wskaznik do interfejsu IShellFolder
if(SHGetDesktopFolder(&pisf) != NOERROR)
{
return;
}
// skonwertuj łańcuch zawierający nazwę foldera
// do postaci wymaganej przez OLE
WideChar oleStr[MAX_PATH];
FDirectory.WideChar(oleStr, MAX_PATH);
unsigned long pchEaten;
unsigned long pdwAttributes;
// uzyskaj obiekt  pidl dla bieżącego foldera
pisf->ParseDisplayName(Handle, 0, oleStr, &pchEaten,
&pidlDirectory, &pdwAttributes);
// uzyskaj wskaznik do interfejsu IShellFolder dla bieżącego foldera
if(pisf->BindToObject(pidlDirectory,NULL,
IID_IShellFolder, (void**)&sfChild) != NOERROR)
{
return;
}
// przeprowadz iterację po plikach w folderze
sfChild->EnumObjects(Handle, SHCONTF_NONFOLDERS |
SHCONTF_INCLUDEHIDDEN, &ppenumIDList);
// przetwórz listę pomocniczą uzyskaną w wyniku iteracji
ULONG celt = 1;
ULONG Fetched = 0;
ppenumIDList->Next(celt, &rgelt, &Fetched);
hExtent = 0;
while(Fetched > 0)
{
// dodaj element do listy
int l = AddItem(rgelt);
if(l > hExtent)
hExtent = l;
ppenumIDList->Next(celt, &rgelt, &Fetched);
}
62
}
}
catch(Exception &E)
{
throw(E); // ponów zaistniały wyjątek
}
}
__finally
{
// wykonaj czynności kończące dla mechanizmów systemowych
g_pMalloc->Free(rgelt);
g_pMalloc->Free(ppenumIDList);
g_pMalloc->Free(pidlDirectory);
pisf->Release();
sfChild->Release();
g_pMalloc->Release();
Items->EndUpdate();
}
// Utwórz poziomy pasek przewijania, jeżeli jest niezbędny
DoHorizontalScrollBar(hExtent);
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)
{
// dodaj minimalny margines
he += 2;
// jeżeli mają być widoczne ikony, dodaj 16 pikseli na ikonę
// i 2 piksele na jej odstęp od tekstu
if(ShowGlyphs)
he += 18;
Perform(LB_SETHORIZONTALEXTENT, he, 0);
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::GetSysImages(void)
{
SHFILEINFO shfi;
DWORD iHnd;
if(!FImages)
{
FImages = new TImageList(this);
FImages->ShareImages = true;
FImages->Height = 16;
FImages->Width = 16;
iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX |
SHGFI_SHELLICONSIZE | SHGFI_SMALLICON);
if(iHnd != 0)
FImages->Handle = iHnd;
}
}
// ---------------------------------------------------------------------------
int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)
{
SHFILEINFO shfi;
int Index;
SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL |
SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_DISPLAYNAME |
SHGFI_USEFILEATTRIBUTES);
63
// zweryfikuj nazwę pod kątem dodania jej do listy
bool FCanAdd = true;
if(FOnAddItem)
FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);
if(FCanAdd)
{
TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);
Index = Items->AddObject(AnsiString(shfi.szDisplayName),
(TObject*)ShellInfo);
// zwróć szerokość nazwy w pikselach
return Canvas->TextWidth(Items->Strings[Index]);
}
// zwróc zero jako sygnał, że nazwa nie została dodana
return 0;
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::DrawItem(int Index, const TRect &Rect,
TOwnerDrawState State)
{
int Offset;
Canvas->FillRect(Rect);
Offset = 2;
if(ShowGlyphs)
{
TShellFileListItem *ShellItem = reinterpret_cast
(Items->Objects[Index]);
// narysuj ikonę przeznaczoną dla pliku
FImages->Draw(Canvas, Rect.Left+2, Rect.Top+2, ShellItem->ImageIndex,
true);
Offset += 18;
}
int Texty = Canvas->TextHeight(Items->Strings[Index]);
Texty = ((ItemHeight - Texty) / 2) + 1;
// wypisz nazwę pliku
Canvas->TextOut(Rect.Left + Offset, Rect.Top + Texty, Items-
>Strings[Index]);
}
//----------------------------------------------------------------------------
void __fastcall TSHFileListBox::DblClick(void)
{
if(FCanLaunch)
{
int ii=0;
//  uruchom wszystkie podświetlone elementy
for(ii=0; ii < Items->Count; ii++)
{
if(Selected[ii])
{
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);
}
}
}
// wygeneruj zdarzenie OnDblClick
if(FOnDblClick)
FOnDblClick(this);
}
//----------------------------------------------------------------------------
64
void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState
Shift,
int X, int Y)
{
if(!FRightBtnSel)
return;
TPoint ItemPos = Point(X,Y);
// czy kursor myszy wskazuje jakąś nazwę?
int Index = ItemAtPos(ItemPos, true);
// nie, nic nie rób
if(Index == -1)
return;
// tak, wybierz wskazywaną pozycję
Perform(LB_SETCURSEL, (WPARAM)Index, 0);
}
//----------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TSHFileListBox *)
{
new TSHFileListBox(NULL);
}
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// The following functions were written by Damon Chandler
//----------------------------------------------------------------------------
LPITEMIDLIST CopyPIDL(LPITEMIDLIST lpidl)
{
unsigned short size = GetPIDLSize(lpidl);
LPITEMIDLIST lpidlCopy = CreatePIDL(size);
MoveMemory(lpidlCopy, lpidl, size);
return lpidlCopy;
}
//----------------------------------------------------------------------------
unsigned short GetPIDLSize(LPITEMIDLIST lpidl)
{
unsigned short cb = 0;
while (lpidl)
{
cb = (unsigned short)(cb + lpidl->mkid.cb);
lpidl = GetNextItem(lpidl);
}
return (unsigned short)(cb + 2);
}
//----------------------------------------------------------------------------
LPITEMIDLIST CreatePIDL(unsigned short size)
{
LPITEMIDLIST lpidlResult;
LPMALLOC lpMalloc;
if (SUCCEEDED(SHGetMalloc(&lpMalloc)))
{
lpidlResult = (LPITEMIDLIST)lpMalloc->Alloc(size);
if (lpidlResult)
ZeroMemory(lpidlResult, sizeof(size));
lpMalloc->Release();
}
return lpidlResult;
}
65
//----------------------------------------------------------------------------
LPITEMIDLIST GetNextItem(LPITEMIDLIST pidl)
{
unsigned short nLen = pidl->mkid.cb;
if(nLen == 0)
return NULL;
return (LPITEMIDLIST)((LPBYTE)pidl + nLen);
}
Tworzenie własnych kontrolek
bazodanowych
Tworzeniem kontrolek przeznaczonych do współpracy z bazami danych  zwanych popularnie
kontrolkami bazodanowymi  rządzą te same zasady, które przestawiliśmy dotychczas w tym
rozdziale. W zależności od charakteru tej współpracy kontrolki te podzielić można na dwie grupy:
umożliwiające jedynie podgląd danych oraz umożliwiające również ich modyfikację.
Materiałem wyjściowym dla tworzonego przez nas komponentu TDBMaskEdit będzie kontrolka
TMaskEdit, umożliwiająca wprowadzanie danych w postaci zgodnej z określoną a priori maską.
Komponent ten wyposażymy najpierw w mechanizmy umożliwiające wyświetlanie danych
zawartych w pliku zródłowym, następnie rozszerzymy jego funkcje o możliwości wprowadzania i
modyfikowania danych.
Połączenie kontrolki z bazą danych
Dla kontrolki dokonującej wyświetlania i ew. edycji danych oczywistą sprawą jest konieczność
zapewnienia mechanizmów łączności z tymi danymi. W bibliotece VCL szeroko pojętą
organizacją dostępu do danych zajmuje się komponent TDataLink, postrzegający zawartość
zbioru danych poprzez komponenty klasy TDataSet i TDataSource.  Skalarny charakter
naszej kontrolki TDBMaskEdit (wyświetlanie pojedynczej wartości) przesądza o zastosowaniu
jej w zasadzie do pojedynczego pola rekordu bazy danych reprezentowanego przez komponent
TField; łącznikiem pomiędzy kontrolką bazodanową a polem w bazie danych jest komponent
TFieldDataLink, wywodzący się z klasy TDataLink.
Aby zrealizować połączenie kontrolki z danymi, konieczne jest wykonanie kolejno trzech
następujących czynności:
" zadeklarowanie właściwości odpowiedzialnych za połączenie  czyli wskazania na obiekt-
łącznik oraz właściwości pokrewnych;
" zaimplementowanie metod dostępowych tej właściwości;
" zainicjowanie obiektu-łącznika.
66
Wykorzystanie klasy TFieldDataLink wymaga dołączenia pliku nagłówkowego
dbctrls.hpp:
#include // dla TFieldDataLink
//---------------------------------------------------------------------------
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
TFieldDataLink *FDataLink;
...
};
Jako że nasz komponent bazuje na konkretnym polu bazy danych, konieczne jest zadeklarowanie
dwóch związanych z tym właściwości: DataSource, określającej powiązany z bazą komponent
TDataSource (reprezentujący bieżący rekord tej bazy) oraz DataField, określającej nazwę
konkretnego pola w tym rekordzie:
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
....
AnsiString __fastcall GetDataField(void);
TDataSource* __fastcall GetDataSource(void);
void __fastcall SetDataField(AnsiString pDataField);
void __fastcall SetDataSource(TDataSource *pDataSource);
....
__published:
__property AnsiString DataField =
{read = GetDataField, write = SetDataField, nodefault};
__property TDataSource *DataSource =
{read = GetDataSource, write = SetDataSource, nodefault};
....
};
Wspominaliśmy na wstępie, iż nasz komponent powinien umożliwiać na żądanie blokadę
modyfikacji danych, udostępniając je wyłącznie do podglądu; ten aspekt jego funkcjonowania nie
jest jednakże elementem łączności z danymi  ustawienie na true właściwości ReadOnly
uniemożliwia jedynie modyfikację wyświetlanej zawartości, co oczywiście nie zabrania
użytkownikowi dokonywania bezpośrednich zapisów do pola bazy danych identyfikowanego
przez właściwości DataSource i DataField.
Implementację metod dostępowych wspomnianych właściwości przedstawia wydruk 7.57.
67
Wydruk 7.57. Implementacja metod dostępowych właściwości połączeniowych
AnsiString __fastcall TDBMaskEdit::GetDataField(void)
{
return(FDataLink->FieldName);
}
TDataSource * __fastcall TDBMaskEdit::GetDataSource(void)
{
return(FDataLink->DataSource);
}
void __fastcall TDBMaskEdit::SetDataField(AnsiString pDataField)
{
FDataLink->FieldName = pDataField;
}
void __fastcall TDBMaskEdit::SetDataSource(TDataSource *pDataSource)
{
if(pDataSource != NULL)
pDataSource->FreeNotification(this);
FDataLink->DataSource = pDataSource;
}
Pewnego komentarza wymaga tu użycie funkcji FreeNotification(). Jak już wcześniej
pisaliśmy, komponent musi być świadom zwalniania obiektów, na które wskazują jego
właściwości. Świadomość tę zapewnia metoda Notification(), lecz tylko w stosunku do
komponentów znajdujących się na tym samym formularzu; zwalnianie komponentów
znajdujących się na innych formularzach nie jest dla niego zauważalne w sposób standardowy i
musi być zapewnione przy użyciu metody FreeNotification(). Komponent, wywołując tę
metodę w kontekście komponentu powiązanego, zapewnia sobie tym samym informację o
destrukcji tego ostatniego. Wykorzystaliśmy tę możliwość, ponieważ nie można wykluczyć, iż
powiązany komponent wskazywany przez właściwość DataSource znajduje się na innym
formularzu.
Nasz komponent nie jest jeszcze kompletny  próba jego wykorzystania w aplikacji skończy się
rychło komunikatem o naruszeniu mechanizmu ochrony (access violation). Oczywistym tego
powodem jest brak egzemplarza obiektu-łącznika wskazywanego przez właściwość
FieldDataLink. Egzemplarz ten powinien być utworzony w konstruktorze komponentu
__fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)
: TMaskEdit(Owner)
{
FDataLink = new TFieldDataLink();
FDataLink->Control = this;
FDataLink->OnUpdateData = UpdateData;
FDataLink->OnDataChange = DataChange;
}
68
Po utworzeniu obiektu-łącznika zainicjowane zostają jego właściwości: Control, wskazująca
macierzystą kontrolkę (czyli nasz komponent) i dwie właściwości zdarzeniowe, którym przypisuje
się funkcje obsługi (będące metodami naszego komponentu).
Egzemplarz obiektu-łącznika powinien być także zwolniony w ramach destruktora, po uprzednim
anulowaniu powiązań z naszym komponentem i jego metodami:
__fastcall TDBMaskEdit::~TDBMaskEdit(void)
{
if(FDataLink)
{
FDataLink->Control = 0;
FDataLink->OnUpdateData = 0;
FDataLink->OnDataChange = 0;
delete FDataLink;
}
}
Różnorodne atrybuty pola bazy danych, z którym połączony jest nasz komponent, dostępne są
poprzez reprezentujący to pole komponent TField. Ten ostatni wskazywany jest przez
właściwość Field, stanowiącą tak naprawdę powielenie właściwości obiektu łącznikowego o tej
samej nazwie. Właściwość ta jest właściwością tylko do odczytu  jej deklaracja nie zawiera
klauzuli write:
__property TField *Field = {read = GetField};
...
TField * __fastcall TDBMaskEdit::GetField(void)
{
return(FDataLink->Field);
}
Aktualizowanie zawartości kontrolki  zdarzenie
OnDataChange
Nasz komponent, mimo połączenia z bazą danych, nie posiada jeszcze mechanizmów
umożliwiających reagowanie na zmiany w powiązanych danych, na przykład  przesunięcie bazy
danych do kolejnego rekordu ( wiersza ). Mechanizmy takie udostępniane są przez klasy
łącznikowe  każda zmiana w połączonych danych powoduje mianowicie wygenerowanie
zdarzenia OnDataChange w kontekście obiektu łącznikowego. Zdarzenie to jest typu
TNotifyEvent, co decyduje o postaci jego funkcji obsługi, będącej de facto metodą
DataChange() naszego komponentu:
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
...
69
void __fastcall DataChange(TObject *Sender);
...
}
......
void __fastcall TDBMaskEdit::DataChange(TObject *Sender)
{
if(!FDataLink->Field) // czy okreslono obiekt łącznikowy?
{
// nie określono obiektu łącznikowego;
// w czasie projektowania zwróć nazwę komponentu,
// w czasie wykonania  pusty łańcuch
if(ComponentState.Contains(csDesigning))
Text = Name;
else
Text = "";
}
else // określono obiekt łacznikowy, zwróć tekstową reprezentację pola
Text = FDataLink->Field->AsString;
}
Metoda DataChange() sprawdza najpierw, czy w ogóle określono obiekt łącznikowy; jeżeli
tak, to właściwości Text (dziedziczonej z klasy bazowej i reprezentującej wyświetlaną
zawartość) nadawana jest tekstowa (AsString) reprezentacja odnośnego pola bazy. Jeżeli
obiektu łącznikowego nie określono  FDataLink jest pustym wskaznikiem  nie ma sensu
pojęcie zawartości powiązanego pola i właściwości Text nadawane są wartości zastępcze: nazwa
komponentu w czasie projektowania i pusty łańcuch w czasie wykonania.
Zapis zmian do bazy  zdarzenie OnUpdateData
Nasz komponent w obecnym kształcie zapewnia jedynie podgląd zawartości powiązanego pola,
aktualizowany dzięki zdarzeniu OnDataChange(). Właściwość ReadOnly posiada domyślną
wartość true, brak jest ponadto mechanizmów powodujących odzwierciedlenie zmian
wyświetlanej zawartości w powiązanej bazie danych.
Oprócz oczywistego  odblokowania komponentu (poprzez ustawienie na false właściwości
ReadOnly) konieczna jest jeszcze modyfikacja odziedziczonej z klasy bazowej (TMaskEdit)
reakcji na zdarzenia pochodzące od myszy i klawiatury. Zachowania te muszą obecnie
odzwierciedlać powiązanie komponentu z bazą danych  w tym sensie, iż wszelkie zmiany w
zawartości komponentu mają sens jedynie wtedy, jeżeli będą mogły być odzwierciedlone
w zawartości powiązanego pola. Spełnienie tego warunku sprawdza się, próbując przełączyć
powiązaną bazę danych w stan edycji  dokonuje tego metoda Edit() obiektu łącznikowego,
zwracająca wartość true, jeżeli przełączenie takie faktycznie nastąpi.
Poniższy wydruk przedstawia zmodyfikowane metody MouseDown() i KeyDown()
odpowiedzialne za reakcję komponentu na czynności wykonywane z udziałem myszy i klawiatury.
W obydwu przypadkach po sprawdzeniu, iż dozwolone jest modyfikowanie zawartości
komponentu (ReadOnly=false) i powiązana baza danych znajduje się w trybie edycji
(FDataLink >Edit()=true), następuje powielenie zachowania odziedziczonego z klasy
70
bazowej; w przeciwnym razie wywoływana jest funkcja obsługi zdarzenia (odpowiednio:
OnMouseDown i OnKeyDown) określona przez użytkownika.
Wydruk 7.58. Obsługa myszy i klawiatury
void __fastcall TDBMaskEdit::MouseDown(TMouseButton Button, TShiftState Shift,
int X, int Y)
{
if(!ReadOnly && FDataLink->Edit())
TMaskEdit::MouseDown(Button, Shift, X, Y);
else
{
if(OnMouseDown)
OnMouseDown(this, Button, Shift, X , Y);
}
}
void __fastcall TDBMaskEdit::KeyDown(unsigned short &Key, TShiftState Shift)
{
// klawisze sterujące kursorem
Set Keys;
Keys = Keys << VK_PRIOR << VK_NEXT << VK_END << VK_HOME << VK_LEFT << VK_UP
<< VK_RIGHT << VK_DOWN;
if(!ReadOnly && (Keys.Contains(Key)) && FDataLink->Edit())
TMaskEdit::KeyDown(Key, Shift);
else
{
if(OnKeyDown)
OnKeyDown(this, Key, Shift);
}
}
Niezależnie jednak od wszelkich działań wykonywanych na zawartości komponentu działania te
pozostałyby bez wpływu na zawartość powiązanego pola, gdyby nie obsługa drugiego z
opisywanych przed chwilą zdarzeń  OnUpdateData(). Mówiąc skrótowo, zdarzenie to
generowane jest w rezultacie zmian poczynionych w ramach kontrolki (do sprawy tej za chwilę
powrócimy) i stanowi okazję do odzwierciedlenia tych zmian w powiązanej bazie danych.
Obsługą tego zdarzenia zajmuje się metoda UpdateData() prezentowana na wydruku 7.59. Po
upewnieniu się, iż obiekt łącznikowy zezwala na modyfikację (FDataLink >CanModify),
zawartość kryjąca się pod właściwością Text naszego komponentu przepisywana jest do pola
bazy danych.
Wydruk 7.59. Zapisanie zmian w bazie danych
void __fastcall TDBMaskEdit::UpdateData(TObject *Sender)
{
if(FDataLink->CanModify)
FDataLink->Field->AsString = Text;
}
71
Powiązanie komponentu z bazą danych wymaga modyfikacji jeszcze jednego aspektu zachowania
komponentu, dziedziczonego z klasy bazowej  mowa tu o metodzie Change(), której zadaniem
jest poinformowanie Windows o zaistniałej zmianie zawartości. Otóż przełączenie bazy w tryb
edycji (o ile nie znajduje się ona już w tym trybie) spowoduje wygenerowanie zdarzenia
OnDataChange, co natychmiast doprowadzi do efektu odwrotnego od zamierzonego 
mianowicie zastąpienie bieżącej wartości komponentu (dokładniej: jego właściwości Text)
wartością odczytaną z bazy danych. Należy więc najpierw zabezpieczyć zarówno zawartość
komponentu, jak i pozycję kursora w oknie edycyjnym i odtworzyć je po udanym przełączeniu w
tryb edycji. Następnie należy ustawić w obiekcie łącznikowym wskaznik informujący o
zmodyfikowaniu zawartości bazy i wywołać odziedziczoną metodę Change().
Wydruk 7.60. Zmodyfikowana metoda Change()
void __fastcall TDBMaskEdit::Change(void)
{
if(FDataLink)
{
// należy zabezpieczyć właściwości Text i SelStart,
// gdyż przełączenie bazy w tryb edycji może je zmienić
AnsiString ChangedValue = Text;
int CursorPosition = SelStart;
if(FDataLink->CanModify && FDataLink->Edit())
{
// przywrócenie właściwości Text i SelStart
Text = ChangedValue;
SelStart = CursorPosition;
FDataLink->Modified(); // poinformowanie o zmianach
}
}
TMaskEdit::Change();
}
Uważny Czytelnik z pewnością zapyta w tym momencie  co tak naprawdę doprowadza do
wygenerowania zdarzenia OnUpdateData() w obiekcie łącznikowym? Otóż w momencie
zakończenia modyfikacji zawartości komponentu, czyli przeniesienia skupienia na inną kontrolkę,
generowany jest komunikat CM_EXIT, który mapowany jest do metody CMExit():
BEGIN_MESSAGE_MAP
...
MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit)
...
END_MESSAGE_MAP(TMaskEdit)
Metoda ta, po zweryfikowaniu zawartości pola edycyjnego i upewnieniu się, że powiązana baza
danych może być modyfikowana, wywołuje metodę UpdateRecord() obiektu łącznikowego:
72
void __fastcall TDBMaskEdit::CMExit(TWMNoParams Message)
{
try
{
ValidateEdit();
if(FDataLink && FDataLink->CanModify)
FDataLink->UpdateRecord();
}
catch(...)
{
SetFocus();
throw;
}
}
Treść metody TFieldDataLink::UpdateRecord() przekłada się głównie na wywołanie
metody UpdateData()(nie mylić z metodą UpdateData() naszego komponentu):
procedure TDataLink.UpdateRecord;
begin
FUpdating := True;
try
UpdateData;
finally
FUpdating := False;
end;
end;
W klasie TFieldDataLink powoduje to wygenerowanie zdarzenia OnUpdateData:
procedure TFieldDataLink.UpdateData;
begin
if FModified then
begin
if (Field <> nil) and Assigned(FOnUpdateData)
then
FOnUpdateData(Self);
FModified := False;
end;
end;
Jakikolwiek wyjątek zaistniały w trakcie tej operacji powoduje przywrócenie skupienia naszemu
komponentowi, czyli powrót do edycji.
Komunikat CM_GETDATALINK
Biblioteka VCL zawiera użyteczny komponent o nazwie TDBCtrlGrid. Umożliwia on
wyświetlanie kolejnych rekordów bazy danych w swobodnym formacie  każdy rekord
73
wyświetlany jest w odrębnej kontrolce, umiejscowionej na dedykowanym temu rekordowi panelu
(rys. 7.5). Po zaktualizowaniu zbioru danych komponent TDBCtrlGrid wysyła do każdego
panelu (i  rekurencyjnie  każdej znajdującej się na tym panelu kontrolki) komunikat
CM_GETDATALINK, w odpowiedzi na który kontrolka powinna zwrócić wskaznik do swego
obiektu łącznikowego (lub zero, gdy takowego nie posiada). Wskaznik ten wykorzystywany jest
następnie do zaktualizowania właściwości DataSource obiektu łącznikowego:
Tu proszę wkleić rysunek AG-9-5.BMP
Rysunek 7.5. Panele kontrolki TDBCtrlGrid
Wydruk 7.61. Generowanie komunikatu CM_GETDATALINK
procedure TDBCtrlGrid.UpdateDataLinks(Control: TControl; Inserting: Boolean);
var
I: Integer;
DataLink: TDataLink;
begin
if Inserting and not (csReplicatable in Control.ControlStyle)
then
DatabaseError(SNotReplicatable);
DataLink := TDataLink(Control.Perform(CM_GETDATALINK, 0, 0));
if DataLink <> nil then
begin
DataLink.DataSourceFixed := False;
if Inserting then
begin
DataLink.DataSource := DataSource;
DataLink.DataSourceFixed := True;
end;
end;
if Control is TWinControl then
with TWinControl(Control) do
for I := 0 to ControlCount - 1 do
UpdateDataLinks(Controls[I], Inserting);
end;
Obsługa komunikatu CM_GETDATALINK wbudowana jest w każdą kontrolkę bazodanową, o
czym można się przekonać, studiując chociażby treść modułu DBCTRLS.PAS. Obsługa taka
wbudowana jest również i w nasz komponent TDBMaskEdit, by mógł on współpracować z
przeglądarką TDBCtrlGrid. Odpowiedni fragment pliku nagłówkowego dokonuje mapowania
komunikatu CM_GETDATALINK do metody CMGetDataLink:
BEGIN_MESSAGE_MAP
...
MESSAGE_HANDLER(CM_GETDATALINK, TMessage, CMGetDataLink)
...
END_MESSAGE_MAP(TMaskEdit)
która to metoda przypisuje wynikowemu polu struktury komunikatu adres obiektu łącznikowego:
74
void __fastcall TDBMaskEdit::CMGetDataLink(TMessage Message)
{
Message.Result = (int)FDataLink;
}
Na tym kończy się proces przekształcania  niezależnej kontrolki edycyjnej TEditMask w
kontrolkę bazodanową.
Rejestracja komponentów
Po skompletowaniu kodu zródłowego komponentu należy umieścić go w palecie komponentów,
realizując wieloetapowy scenariusz zwany popularnie rejestracją.
Aby komponent mógł zostać zarejestrowany, nie może posiadać niezaimplementowanych,
 czystych (ang. pure) metod wirtualnych (lub dynamicznych)  metody takie deklaruje się często
w klasach przeznaczonych nie do bezpośredniego wykorzystania, lecz na potrzeby tworzenia
nowych komponentów, pozostawiając ich implementację klasom pochodnym. Deklaracja metody
czysto wirtualnej ma następującą postać:
virtual __fastcall () = 0;
przy czym zamiast słowa kluczowego virtual wystąpić może słowo DYNAMIC. W celu
stwierdzenia, czy klasa danego komponentu zawiera takie deklaracje, można przeprowadzić
bezpośrednią analizę jej pliku nagłówkowego, można także powierzyć tę kontrolę jakiejkolwiek
funkcji próbującej utworzyć egzemplarz odnośnego komponentu; w generowanym przez IDE
kodzie funkcja taka nosi nazwę ValidCtrCheck(), a jej parametrem wywołania jest wskaznik
na egzemplarz komponentu.
Gdybyśmy przykładowo chcieli poddać opisanej kontroli komponent klasy TMyComponent,
definicja wspomnianej funkcji miałaby postać:
static inline void ValidCtrCheck(TMyComponent *)
{
new TMyComponent(NULL);
}
Kompilator napotkawszy tę definicję, zasygnalizowałby dwa następujące błędy:
E2352 Cannot create instance of abstract class 'TCustomComponent'
E2353 Class 'TCustomComponent' is abstract because of 'function=0'
Drugi z komunikatów zawierałby nazwę niedozwolonej funkcji i mógłby wystąpić wielokrotnie;
obydwa komunikaty odnosiłyby się do tej samej instrukcji, stanowiącej treść funkcji
ValidCtrCheck.
75
Rejestracja komponentu dokonywana jest przez funkcję o nazwie Register, definiowaną w
module zródłowym komponentu. Definicja tej funkcji musi być zamknięta w przestrzeni nazw
(namespace)  nazwa tej przestrzeni musi być zgodna z nazwą modułu zródłowego i musi
rozpoczynać się z wielkiej litery; reszta nazwy przestrzeni musi być pisana w całości małymi
literami. Oto przykład funkcji rejestrującej komponent TDBMaskEdit zdefiniowany w module
zródłowym DbMaskEdit.cpp:
namespace Dbmaskedit
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TDBMaskEdit)};
RegisterComponents("MJF Pack", classes, 0);
}
}
Nazwa funkcji (Register) musi być obowiązkowo poprzedzona wywołaniem makra PACKAGE.
Zasadniczą treścią funkcji Register() jest wywołanie funkcji RegisterComponents(),
dokonującej fizycznej rejestracji wskazanych komponentów na wskazanej stronie palety
komponentów. Deklaracja tej funkcji znajduje się w pliku nagłówkowym classes.hpp i ma
następującą postać:
extern PACKAGE void __fastcall RegisterComponents
(const AnsiString Page,
TMetaClass* const * ComponentClasses,
const int ComponentClasses_Size
);
Pierwszy parametr określa nazwę strony (w palecie komponentów), na której umieszczone zostaną
rejestrowane komponenty. Jeżeli strona o podanej nazwie jeszcze nie istnieje, zostanie
automatycznie utworzona.
Drugi parametr jest tablicą, której elementy określają typy rejestrowanych komponentów. Każdy
element jest wartością typu TMetaClass*, uzyskiwaną za pomocą operatora __classid, na
przykład:
TMetaClass* MyRegComponents[3] = {
__classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
};
Zamiast TMetaClass* można użyć synonimu TComponentClass:
typedef TMetaClass* TComponentClass;
76
Ostatni parametr wywołania funkcji RegisterComponents() jest indeksem ostatniego
elementu tablicy  jego wartość jest więc o jeden mniejsza od liczby rejestrowanych
komponentów. Oto przykładowa rejestracja ww. trójki komponentów:
namespace Agcomponents
{
void __fastcall PACKAGE Register()
{
TMetaClass* MyRegComponents[3] = {
__classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
};
RegisterComponents("AGcomponents", MyRegComponents, 2);
}
}
W celu utworzenia tablicy rejestrowanych komponentów wygodnie będzie posłużyć się makrem
OPENARRAY, zdefiniowanym w pliku nagłówkowym sysopen.h, symulującym tzw. tablicę
otwartą (open array) Object Pascala  przedstawione wywołanie funkcji
RegisterComponents() upraszcza się wówczas do postaci:
RegisterComponents("MyCustomComponents",
OPENARRAY( TMetaClass*,
( __classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
)
)
);
Jak widać, wywołanie makra OPENARRAY zastępuje drugi i trzeci parametr wywołania funkcji.
Definicja makra OPENARRAY ogranicza do 19 wielkość generowanej tablicy, a więc i liczbę
komponentów rejestrowanych w jednym wywołaniu funkcji RegisterComponents(). Nie
jest to wielkim problemem, ponieważ funkcję RegisterComponents() wywoływać można
wielokrotnie w treści funkcji Register().
Oprócz kompletnych komponentów procesowi rejestracji podlegać także mogą specyficzne typy
właściwości oraz specjalizowane edytory komponentów. Zagadnieniem tym zajmiemy się
szczegółowo w następnym rozdziale.


Wyszukiwarka

Podobne podstrony:
863 03
ALL L130310?lass101
Mode 03 Chaos Mode
2009 03 Our 100Th Issue
jezyk ukrainski lekcja 03
r07 04 ojqz7ezhsgylnmtmxg4rpafsz7zr6cfrij52jhi
DB Movie 03 Mysterious Adventures
Szkol Okres pracodawców 03 ochrona ppoż
Fakty nieznane , bo niebyłe Nasz Dziennik, 2011 03 16
2009 03 BP KGP Niebieska karta sprawozdanie za 2008rid&657
Gigabit Ethernet 03
Kuchnia francuska po prostu (odc 03) Kolorowe budynie

więcej podobnych podstron