Rozdział 14. Komponenty VCL i CLX

Adam Boduch

Zanim przejdziemy do właściwego procesu tworzenia komponentów, konieczne będzie dokładne zapoznanie się z zasadami ich działania. Co prawda zarówno o klasach, jak i o VCL mówiłem już w rozdziale 3., lecz tamta wiedza była konieczna do dalszego projektowania aplikacji w Delphi. Tym razem poznasz architekturę komponentów VCL oraz CLX ze strony projektanta dowiesz się, jak to wszystko działa i jak jest ze sobą połączone.

1 Czym jest komponent?
2 VCL
3 CLX
     3.1 Tworzenie aplikacji opartych na CLX
     3.2 CLX a VCL
     3.3 Architektura CLX
4 Windows a Linux
     4.4 Kompilacja warunkowa
5 Rodzaje komponentów
     5.5 Komponenty wizualne
     5.6 Komponenty niewizualne
     5.7 Komponenty graficzne
6 Hierarchia komponentów
     6.8 TObject
          6.8.1 Metody klasy TObject
                    6.8.1.1 ClassName
                    6.8.1.2 ClassNameIs
                    6.8.1.3 ClassInfo
                    6.8.1.4 ClassParent
                    6.8.1.5 ClassType
                    6.8.1.6 FieldAddress
                    6.8.1.7 MethodAddress
     6.9 TPersistent
          6.9.2 Metoda Assign
          6.9.3 Metoda AssignTo
          6.9.4 Metoda DefineProperties
     6.10 TComponent
          6.10.5 Właściwości klasy TComponent
                    6.10.5.8 ComponentCount
                    6.10.5.9 ComponentIndex
                    6.10.5.10 Components
                    6.10.5.11 ComponentState
                    6.10.5.12 Name
                    6.10.5.13 Owner
          6.10.6 Metody klasy TComponent
     6.11 TControl
          6.11.7 Właściwości klasy TControl
     6.12 TWinControl i TWidgetControl
          6.12.8 Właściwości klas TWinControl i TWidgetControl
          6.12.9 Zdarzenia
     6.13 Klasy TCustom
     6.14 TGraphicControl
7 Budowa komponentu
     7.15 Właściwości
          7.15.10 Rodzaje właściwości
     7.16 Zdarzenia
     7.17 Metody
8 RTTI
     8.18 Właściwości obiektu
     8.19 Dokładniejsze informacje o obiekcie
9 Podsumowanie

W tym rozdziale:
*dowiesz się, jak funkcjonuje klasa VCL;
*dowiesz się, czym jest biblioteka CLX;
*poznasz elementarne metody i właściwości podstawowych klas.

Czym jest komponent?

Delphi jako środowisko wizualne udostępnia różne kontrolki tzw. komponenty, służące do szybkiego projektowania aplikacji. Komponenty są klockami, którymi posługuje się programista w procesie budowania aplikacji.

Patrząc na komponenty ze strony projektanta aplikacji, mamy do czynienia z wygodnymi i łatwymi w obsłudze narzędziami. Nie interesuje nas, jak one działają i jaki kod znajduje się w środku. Ważne są zdarzenia, metody, właściwości i zadania, jakie spełniają. W rzeczywistości wiele komponentów jest ze sobą powiązanych i korzysta z tych samych modułów, klas i bibliotek.

Kluczem do poznania VCL oraz CLX jest zrozumienie zasady działania różnych typów danych, poznanie hierarchii klas itp.

VCL

VCL to Visual Component Library (wizualna biblioteka komponentów). Programowanie w Delphi nie byłoby takie łatwe dla początkujących, gdyby nie VCL. Poprzednikiem VCL była biblioteka OWL (Object Windows Library), znajdująca się w Turbo Pascalu. Dla użytkownika wywołanie konstruktora Create to tylko jeden wiersz kodu, powodujący utworzenie komponentu (obiektu). W rzeczywistości na podstawie kodu VCL wykonywany jest szereg innych procedur, które ostatecznie dają efekt w postaci utworzenia komponentu.

CLX

Opublikowanie Delphi 6 było czymś niezwykłym ze względu na wprowadzenie biblioteki CLX (Component Library for Cross-Platform), czyli międzyplatformowej biblioteki komponentów. Pojawienie się CLX wiązane jest z wydaniem nowego środowiska programistycznego Kylix, przeznaczonego dla systemu operacyjnego Linux. Programowanie w Kyliksie jest niezwykle podobne do programowania w Delphi nawet interfejs jest ten sam. Kylix również tak jak Delphi wykorzystuje język Object Pascal, co mogło trochę dziwić w momencie edycji produktu. W końcu Linux jest platformą, na której króluje C++.

W każdym razie w związku z pojawieniem się Kyliksa oraz biblioteki CLX przekroczona została pewna bariera- od tej pory możliwe jest tworzenie aplikacji zgodnych zarówno z systemem Windows, jak i Linuks.

Tworzenie aplikacji opartych na CLX

Aby stworzyć aplikację działającą w oparciu o CLX, wystarczy z menu File wybrać polecenie New/CLX Application. Wówczas zmieniona zostanie zawartość palety komponentów oraz edytora kodu.

CLX a VCL

Sam akronim VCL może dawać do zrozumienia, że mamy do czynienia z całkowicie wizualną biblioteką. W rzeczywistości tak nie jest, bo VCL zawiera również komponenty, które nie są widoczne, ale jednak odpowiadają za wykonywanie jakichś czynności. Pisząc programy działające w systemie Windows, korzystamy ze standardowych kontrolek tego systemu (chociażby kontrolka TButton), zawartych w bibliotece User32.dll lub CommCtrl32.dll. Natomiast w przypadku CLX komponenty z tej biblioteki są oparte na tzw. widgetach, zawartych w bibliotece Qt. Owa biblioteka zawiera niezależne klasy, które swoim wyglądem oraz funkcjonalnością przypominają standardowe kontrolki Windows.

Słowo widget jest skrótem słów Window Gadget.

Architektura CLX

Architektura CLX jest nieco bardziej złożona niż VCL. Mamy tu bowiem kilka grup komponentów. Oprócz grupy wizualnej VisualCLX istnieją jeszcze komponenty BaseCLX (moduły wspólne dla Delphi i Kyliksa), DataCLX (komponenty zapewniające dostęp do technologii dbExpress będziemy o tym mówić w kolejnej części książki) oraz NetCLX (technologie internetowe).

VCL jest zbudowany na bazie kontrolek WinAPI, zawartych m.in. w bibliotekach User32.dll oraz ComCtrl32.dll. W przypadku CLX grupa komponentów VisualCLX jest oparta na bibliotece Qt.

Hierarchia klas jest ona bardzo podobna do hierarchii komponentów VCL. Dodano parę nowych klas, inne zamieniono miejscami, lecz starano się zachować maksymalną kompatybilność pomiędzy CLX i VCL tak, aby przenoszenie kodu odbywało się z jak najmniejszym nakładem pracy.

Windows a Linux

Można by przypuszczać, że dla programisty tworzącego swoje aplikacje w języku Object Pascal nie ma znaczenia, na jakiej platformie będzie uruchamiany ów program. Oczywiście należy jednak pamiętać o pewnych elementach charakterystycznych dla danego systemu. Przykładem może być konieczność zwracania uwagi na nazewnictwo plików w Linuksie w systemie tym rozróżniane są np. nazwy modułów na liście Uses.
*W systemie Linux używany jest inny znak separatora katalogów. W Windows jest to znak backslash (), a w Linuksie slash (/). Właściwy dla konkretnej platformy znak można odczytać ze stałej PathSeparator.
*Pamiętasz, jak podczas omawiania bibliotek DLL wspominałem o konwencjach wywołania Stdcall, Safecall i Cdecl W Kyliksie należy zmienić tę klauzulę na Cdecl, gdyż tylko taka forma jest prawidłowa.
*Tworząc programy w Kyliksie, należy unikać modułów oraz funkcji charakterystycznych dla Windows np. modułów Windows, ActiveX, ComServ, ComObj itp. gdyż nie odnajdziemy tam takich technologii jak BDE, COM czy ActiveX.
*Bardzo ważne jest zapamiętanie, że w Linuksie nie możemy korzystać z komunikatów. Niedopuszczalne jest zatem wysyłanie komunikatów czy ich przechwytywanie np. WM_CHAR itp.
*Większość klas nazywa się tak samo. Pamiętaj jednak, że klasa TWinControl w CLX nazywa się TWidgetControl. Ze względów kompatybilności zachowano jednak również klasę TWinControl, która jest równoważna klasie TWidgetControl.
*Unikaj stosowania funkcji typowych dla Windows: Win32Check, RaiseLastWin32Error.
*W systemie Linux nie ma Rejestru, nie możesz więc korzystać z klasy TRegistry.
*Nazwy modułów CLX rozpoczynają się od litery Q np. QControls.
*System Linux nie używa liter do określania dysków, tak jak ma to miejsce w Windows. Wszystko opiera się na charakterystycznym systemie plików, gdzie istnieją tylko katalogi główne np. /usr

Kompilacja warunkowa

Zarówno w Delphi, jak i w Kyliksie istnieje możliwość wprowadzenia charakterystycznego dla danej platformy kodu. Wszystko dzięki tzw. symbolom kompilacji warunkowej.

{$IFDEF LINUX}
// kod dla Linuksa
{$ENDIF}

Dla systemu Linux takim symbolem jest LINUX, a dla Windows może to być WIN32 i MSWINDOWS:

{$IFDEF WIN32}
// kod dla Windows
{$ENDIF}

Dzięki takiemu prostemu rozwiązaniu można wprowadzać charakterystyczne dla danej platformy elementy kodu.

Rodzaje komponentów

Komponenty w Delphi dzielą się na wizualne oraz niewizualne ? bynajmniej nie chodzi tutaj o właściwość Visible, której zmiana na Falsepowoduje ukrycie komponentu.

Komponenty wizualne

Nieprzypadkowo dla określenia komponentu wizualnego często używam w tej książce słowa kontrolka (control). Należy zdać sobie sprawę z tego, że kontrolka to obiekt wizualny, będący w stanie odbierać komunikaty od użytkownika. Kontrolki mają właściwości mogące określić ich położenie względem projektanta formularzy. Mogą być także aktywowane (focus). Komponenty wizualne wywodzą się z klasy TControl. Przykładami kontrolek wizualnych są TButton, TLabel, TListView, TComboBox itp.

Komponenty niewizualne

Komponentem jest każdy obiekt, mogący znaleźć się w palecie komponentów. Niektóre z tych obiektów są widoczne po uruchomieniu programu, niektóre jednak nie. Komponenty niewidoczne wywodzą się z klasy TComponent, lecz mimo tego, że ich nie widać, pełnią inną funkcję, często o wiele ważniejszą niż rola obiektów wizualnych. Przykładem komponentów niewidocznych jest komponent TTimer, TOpenDialog (ogólnie wszystkie komponenty z palety Dialogs) oraz komponenty z palety Indy.

Komponenty graficzne

Niektóre komponenty Delphi mimo swego charakteru wizualnego nie posiadają zdolności interakcji z użytkownikiem ? nie mogą więc stać się aktywne. Nie posiadają także uchwytu, a ich klasą bazową jest TGraphicControl.
Przykładem takich komponentów są TPaintBox i TImage.

Hierarchia komponentów

Na rysunku 14.1 przedstawiona została hierarchia komponentów VCL. Naturalnie jest to tylko fragment ogromnej pajęczyny klas.

14.1.jpg
Rysunek 14.1. Hierarchia komponentów VCL

W CLX ze względów kompatybilności starano się zachować identyczne nazewnictwo klas. Różnica pojawia się w klasie TWinControl, która w CLX nosi nazwę TWidgetControl. Pamiętaj o tym, że w CLX nie ma klasy TRegistry, lecz możesz korzystać z klasy TRegINIFile.

TObject

Klasa TObject jest podstawowym fundamentem całej struktury VCL oraz CLX. Zawiera wiele podstawowych metod, obecnych we wszystkich klasach. Wszystko dzięki dziedziczeniu ? w czasie, gdy pozostałe klasy dziedziczą po klasie TObject, przejmują jej wszystkie metody. Klasa ta odpowiada za tak podstawowe czynności, jak:
*tworzenie i usuwanie instancji komponentu,
*alokację i zwalnianie potrzebnej pamięci,
*obsługę komunikatów,
*zwracanie informacji na temat rodzaju klasy, nazwy itp.

Metody klasy TObject

Klasa TObject definiuje jedynie metody, które mogą być później używane przez wszystkie inne kontrolki VCL. Spis najważniejszych metod klasy TObject znajduje się poniżej.

ClassName
class function ClassName: ShortString;

Owa funkcja zwraca nazwę klasy ? przykład:

  ShowMessage(Button1.ClassName);

Powyższy kod spowoduje wyświetlenie rzeczywistej nazwy klasy, czyli TButton.

ClassNameIs
class function ClassNameIs(const Name: string): Boolean;

Funkcja sprawdza, czy podana w parametrze klasa odpowiada tej, z której jest wywoływana funkcja. Najlepiej objaśnić to na przykładzie:

Button1.ClassNameIs(?TButton?); // funkcja zwróci True
Button1.ClassNameIs(?TObject?); // funkcja zwróci False
ClassInfo
class function ClassInfo: Pointer;

Funkcja zwraca wskaźnik do tablicy zawierającej informacje takie jak typ obiektu, właściwości itp. Wrócimy do tego nieco później.

ClassParent
class function ClassParent: TClass;

Informacja o klasie bazowej dla naszego komponentu jest zwracana przez funkcję ClassParent. Dzięki owej funkcji możemy odwołać się do innych właściwości klasy bazowej, nie znając jej podczas tworzenia programu.

ClassType
function ClassType: TClass;

Funkcja działa podobnie jak ClassParent. Jedyna różnica polega na tym, że zwraca ona informacje na temat klasy, z której jest wywoływana ? np.:

  ShowMessage(Button1.ClassType.ClassName);

Po wykonaniu takiego kodu w okienku wyświetlony zostanie napis TButton. Inna sprawa, że taki sam efekt uzyskamy, korzystając z funkcji ClassName.

FieldAddress
function FieldAddress(const Name: ShortString): Pointer;

Funkcja FieldAddress zwraca wskaźnik do właściwości znajdującej się w pamięci. Nazwa właściwości musi zostać podana w parametrze.

MethodAddress
class function MethodAddress(const Name: ShortString): Pointer;

Funkcja działa podobnie jak FieldAddress, tyle że ta zwraca wskaźnik do metody umieszczonej w pamięci ? nie zaś właściwości.

Więcej opisów metod klasy TObject możesz znaleźć w systemie pomocy Delphi lub (w języku polskim) pod adresem Delphi

TPersistent

Klasą drugą po TObject w całej hierarchii jest TPersistent. Oczywiście większość metod dziedziczy po TObject, ale wprowadza także swoje metody.

Klasy TPersistent możesz użyć jako klasy bazowej w momencie, gdy deklarujesz klasy, które nie mają być komponentem. Jaka jest więc różnica między TObject a TPersistent? Otóż klasa TPersistent może być przodkiem dla wszystkich klas mogących zapisywać wartości do właściwości, które następnie mogą być przechowywane w pliku *.dfm (lub *.xfm ? dla Kyliksa).

Metoda Assign

procedure Assign(Source: TPersistent); virtual;

Metoda Assign, obecna w klasie TPersistent, powoduje skopiowanie danych (właściwości) z jednego obiektu do drugiego.

Metoda AssignTo

procedure AssignTo(Dest: TPersistent); virtual;

AssignTo działa odwrotnie niż procedura Assign. Powoduje skopiowanie danych do innego obiektu, określonego w parametrze Dest.

Metoda DefineProperties

procedure DefineProperties(Filer: TFiler); virtual;

Procedura DefineProperties umożliwia przechowanie w pliku *.dfm dodatkowych danych ? tym pojęciem zajmiemy się kolejnym rozdziale.

TComponent

Klasa TComponent, jak można wywnioskować z nazwy, jest przodkiem wszystkich komponentów. Komponenty dziedziczące po TComponent mogą:
*być zintegrowane z IDE Delphi, w tym mogą być umieszczone na palecie komponentów;
*jeden obiekt typu TComponent może posiadać inny obiekt (być jego rodzicem);
*komponenty mogą być konwertowane na obiekty COM lub kontrolki ActiveX.
Obiekty COM są wynalazkiem firmy Microsoft, stąd obecne są jedynie na platformie Windows.

Klasa TComponent wprowadza szereg nowych właściwości oraz metod, obecnych później we wszystkich komponentach (w końcu wszystkie komponenty pochodzą od klasy TComponent).

Właściwości klasy TComponent

Klasa TComponent zawiera wiele ciekawych właściwości, które przedstawiłem poniżej.

ComponentCount
property ComponentCount: Integer;

Właściwość ComponentCount zwraca liczbę obiektów umieszczonych na danym komponencie (rysunek 14.2.):

procedure TMainForm.Button1Click(Sender: TObject);
begin
  Application.MessageBox(PChar('Na formularzu znajdują się ' + IntToStr(ComponentCount) + ' komponenty),
  'Tak', MB_OK)
end;

14.2.jpg
Rysunek 14.2. Program demonstrujący znaczenie właściwości ComponentCount

ComponentIndex
property ComponentIndex: Integer;

Inna właściwość klasy TComponent ? Components[] to tablica komponentów. Do konkretnego obiektu można się odwołać, podając jego indeks. Taki indeks natomiast jest zwracany przez właściwość ComponentIndex.

  ShowMessage(IntToStr(
     Button1.ComponentIndex));

W powyższym przypadku w okienku pojawi się cyfra 1 jako numer identyfikacyjny dla komponentu Button1.

Components
property Components[Index: Integer]: TComponent;

Jak wspomniałem powyżej, jest to tablica wszystkich elementów (komponentów) umieszczonych na danym obiekcie. Do konkretnej kontrolki można się odwołać, podając w nawiasie kwadratowym jej indeks:

  Components[1].Free;

Powyższy kod spowoduje zniszczenie obiektu określonego numerem 1.

ComponentState
property ComponentState: TComponentState;

Sygnalizuje stan komponentu. Może zwrócić wartości takie, jak przedstawione w tabeli 14.1.

Tabela 14.1. Możliwe elementy typu TComponentState

ElementOpis
csDesigningKomponent jest w trakcie projektowania, np. jego właściwości są zmieniane
csDestroyingKomponent za chwilę zostanie zniszczony
csFixupsKomponent jest podłączony do innego komponentu znajdującego się na innym formularzu
csLoadingKomponent jest właśnie ładowany (tworzony)
csReadingKomponent odczytuje swoje właściwości ze strumienia (pliku *.dfm)
csWritingKomponent zapisuje właściwości do strumienia
Name
property Name: TComponentName;

Definiuje unikatową w ramach formularza nazwę dla komponentu. Wartość jest przeznaczona zarówno do zapisu, jak i do odczytu.

Owner
property Owner: TComponent;

Właściwość Owner jest wskazaniem obiektu-rodzica. Jeżeli przykładowo komponent kkjest umieszczony na formularzu Form1, to dzięki takiej konstrukcji:

 Button1.Owner...

można odwołać się do formularza Form1.

Metody klasy TComponent

Nowych metod wprowadzonych wraz z klasą TComponent jest bardzo wiele. Większość z nich być może nigdy nie zostanie przez Ciebie użyta, gdyż służą np. do określenia pewnego stanu, w jakim ma znajdować się komponent.

Na pewno najważniejszą metodą klasy TComponent jest konstruktor, który w odróżnieniu od klasy TObject posiada dodatkowy parametr ? rodzica, czyli obiekt, na którym ma zostać utworzony nowy komponent.

constructor Create(AOwner: TComponent); virtual;

Utworzony w ten sposób konstruktor powinien zostać zwolniony metodą Free.

Do zwalniania komponentu nigdy nie używaj metody Destroy. Procedura Free przed rzeczywistym zwolnieniem sprawdza, czy obiekt nie został już wcześniej zwolniony, co zapobiega pojawieniu się błędu typu Access Violation.

Oto przykład utworzenia komponentu typu TButton w sposób dynamiczny:

procedure TForm1.btnMakeClick(Sender: TObject);
var
  MyButton : TButton;
begin
  MyButton := TButton.Create(Form1);
  MyButton.Parent := Form1; // określenie rodzica
  MyButton.Caption := 'Przycisk 1';
end;

Rodzicem komponentu ustanawiamy formularz Form1. Jeżeli nie określiliśmy dokładniejszych danych ? np. położenia komponentu ? zostanie on umieszczony w lewym, górnym rogu formularza.

Zwróć uwagę, że w powyższym kodzie nie ma instrukcji zwalniającej komponent ? Free. Wszystko dzięki temu, że rodzicem nowego obiektu został formularz ? Form1. Przy zwalnianiu (zamykaniu) okna program najpierw zwolni obiekty znajdujące się na nim, czyli m.in. nasz przycisk.

Ciekawą metodą klasy TComponent jest procedura FindComponent. Wspominam tu o niej, gdyż być może często będziesz korzystał z jej zalet. Umożliwia ona bowiem odnalezienie jakiegoś komponentu na formularzu i odwołanie się do konkretnych jego metod ? oto przykład:

procedure TMainForm.btnMakeClick(Sender: TObject);
begin
  Randomize;
  TEdit(FindComponent('Edit' + IntToStr(Random(3)+1))).Text := 'Nowa wartość';
end;

Powyższy kod spowoduje losowe odwołanie się do którejś z trzech umieszczonych na formularzu kontrolek typu TEdit ki zmianę jej właściwości Text (rysunek 14.3).

14.3.jpg
Rysunek 14.3. Losowe odwołanie się do której z kontrolek TEdit

TControl

Kolejną klasą w hierarchii obiektów VCL jest klasa TControl. Reprezentuje ona komponenty wizualne (jest podstawową klasą dla tego rodzaju komponentów). Każdy obiekt typu TControl posiada dodatkowe właściwości określające położenie komponentu: Top (położenie w pionie), Left (położenie w poziomie), Width (szerokość) i Height (wysokość).

Właściwości klasy TControl

Najważniejsze z właściwości (których nazwy możesz już pamiętać, gdyż są one wyświetlane w Inspektorze Obiektów) przedstawiłem w tabeli 14.2.

Tabela 14.2. Najważniejsze właściwości klasy TControl

WłaściwośćOpis
AlignOkreśla wyrównanie komponentu względem obiektu-rodzica
AutoSizeOkreśla wyrównanie komponentu względem obiektu-rodzica
CaptionTytuł komponentu (tekst wyświetlany na obiekcie)
ClientHeightRozmiar obszaru roboczego (wysokość)
ClientWidthRozmiar obszaru roboczego (szerokość)
ColorKolor tła obiektu
CursorKursor wyświetlany po umieszczeniu wskaźnika myszy nad obiektem
EnabledOkreśla, czy obiekt jest aktywny czy też nie
FontCzcionka używana przez komponent
HintWskazówka (etykietka podpowiedzi), pokazywana po umieszczeniu kursora nad obiektem
ShowHintWłaściwość określa, czy podpowiedzi mają być wyświetlane
VisibleWłaściwość określa, czy komponent ma być widoczny podczas działania programu

Procedury i funkcje klasy TControl mają na celu przede wszystkim ustawienie danej wartości komponentu lub odczytanie jej. Owa klasa posiada także wiele metod nakazujących w konsekwencji wystąpienie pewnego zdarzenia (klasa TControl wprowadza zdarzenia, dzięki którym jesteśmy w stanie zareagować na konkretne sytuacje).

TWinControl i TWidgetControl

Klasy TWinControl (VCL) oraz TWidgetControl (CLX) są bazowymi klasami dla kontrolek takich, jak przycisk (TButton) i lista rozwijalna (TComboBox). Owe klasy są klasami bazowymi dla bardzo wielu kontrolek w Delphi. Charakteryzują się następującymi właściwościami:
*Umożliwiają wyświetlanie tekstu (np. TMemo).
*Umożliwiają odbieranie od użytkownika zdarzeń, reagowanie np. na naciśnięcie przycisku czy wpisywanie znaków za pomocą klawiatury.
*Kontrolki mogą być rodzicem innych obiektów.
*Kontrolki pochodzące z tych klas posiadają uchwyt (VCL) lub widget (CLX).

Właściwości klas TWinControl i TWidgetControl

Obie klasy definiują wiele właściwości, mających na celu zmodyfikowanie wyglądu danej kontrolki. Za przykład może tu posłużyć właściwość BevelKind, która określa styl rysowania ramki dla komponentu. Do podobnych właściwości tego typu należą także: BevelEdges, BevelInner, BevelOuter, BevelWidth i BorderWidth.

Ciekawą właściwością jest także Brush, która określa kolor i styl wypełniającego wnętrze komponentu.

Zdarzenia

Zdarzenia klasy TWinControl są związane z uzyskiwaniem dostępu do obiektu (aktywacja) czy też wprowadzaniem tekstu z klawiatury. Są to zdarzenia: OnDockDrop, OnDockOver, OnEnter, OnExit, OnKeyDown, OnKeyPress i OnKeyUp.

Klasy TCustom

Wiele klas VCL ? np. TMemo ? posiada odpowiedniki w postaci klas bazowych, które rozpoczynają się przedrostkiem Custom, np. TCustomMemo. Takie klasy pełnią wyłącznie rolę klas bazowych dla nowych komponentów i nie są w praktyce wykorzystywane. Np. komponent TDBMemo także wywodzi się z klasy TCustomMemo ? dzięki temu rozwiązaniu programiści oszczędzają wiele wierszy kodu, dziedzicząc w zwykły sposób potrzebne metody i właściwości.

TGraphicControl

Jest to raczej specyficzny rodzaj kontrolek, a mianowicie komponent graficzny. Komponenty takie są oczywiście komponentami wizualnymi, ale nie mogą być aktywne. Nie mogą także pełnić roli rodzica innych komponentów. Śmiało można powiedzieć, że kontrolki tego typu są komponentami nieaktywnymi.

Natomiast każda kontrolka typu TGrpaphicControl ma przyporządkowane płótno (Canvas), którego odświeżenie następuje za pomocą wywołania metody Repaint.

Budowa komponentu

Komponent w rzeczywistości jest klasą. Jest jednak klasą dość specyficzną, posiadającą określoną budowę, bez której taka klasa nie byłaby po prostu komponentem.

Właściwości

W klasie właściwości służą do przechowywania w pamięci pewnej wartości. W przypadku komponentów wartości standardowo wprowadzone w Inspektorze Obiektów są zapisywane w pliku *.dfm. Jednakże aby właściwości były widoczne w Inspektorze Obiektów, należy użyć specjalnej sekcji ? Published. O klasach mówiłem już w rozdziale 3., lecz o sekcji published jedynie wówczas wspomniałem. Jeśli tworzysz komponenty i chcesz dodać do nich właściwości, kod w sekcji published musi wyglądać tak:

TMojKomponent = class(TLabel)
private
  FText : String;
  procedure DoIt(Value : String);
published
  properties Text : String read FText write DoIt;  
end;

Właściwości w klasie musimy poprzedzać słowem kluczowym properties. Powyższy kod oznacza, że właściwość Text będzie typu String, a jej odczytanie będzie równoważne z odczytaniem danych ze zmiennej FText. Wszystko dzięki klauzuli read, po której następuje nazwa zmiennej umieszczonej w sekcji Private (nie jest konieczne umieszczenie zmiennej FText w sekcji Public). Natomiast w przypadku, gdy użytkownik zechce przypisać jakąś wartość właściwości Text, wywołaprocedurę DoIt. Nowa wartość zostanie przekazana do procedury DoIt w parametrze Value.

Warto, abyś przyzwyczaił się do takiej konstrukcji w przypadku komponentów, gdyż w przyszłości często będziemy ją stosowali.

Chcąc uczynić daną właściwość przeznaczoną tylko do zapisu lub tylko do odczytu, wystarczy pominąć jedną z klauzul ? albo read, albo write.

Istnieje możliwość dodania na samym końcu słowa kluczowego default, które powoduje ustawienie domyślnej wartości właściwości.

Rodzaje właściwości

Pracując już jakiś czas z Delphi, zapewne zauważyłeś, że nie wszystkie właściwości w Inspektorze Obiektów są przedstawione tak samo. Niektóre proste właściwości służą jedynie do wpisywania nowej wartości i odczytywania jej. Z kolei inne właściwości są przedstawione w postaci listy rozwijalnej. Tabela 14.3 przedstawia różne typy właściwości.

Tabela 14.3. Typy właściwości

Typy właściwościOpis
WyliczeniowaWłaściwość wyliczeniowa prezentowana jest w Inspektorze Obiektów w formie listy rozwijalnej (łączenie z typem `Boolean`) ? rysunek 14.4
ZbiorowaWłaściwość zbiorowa (`set`) jest prezentowana w formie listy rozwijalnej. Wartości w zbiorze można zmieniać na `True `lub `False`, w zależności od tego, czy właściwość przynależy do zbioru czy też nie (rysunek 14.5)
ObiektowaCzasami zdarza się, że właściwość danego komponentu jest konkretnym obiektem (klasą). Tak jest np. z właściwością Lines komponentu `TMemo`, która jest właściwością typu `TStringList`. Wówczas przy próbie edycji otwierany zostaje nowy edytor

14.4.jpg
Rysunek 14.4. Właściwość wyliczeniowa

14.5.jpg
Rysunek 14.5. Właściwość zbiorowa

Zdarzenia

Zdarzenia służą do zaprogramowania określonej reakcji w momencie zajścia jakiegoś wydarzenia (np. kliknięcia myszą). Podczas projektowania własnych komponentów dość często będziesz zmuszony do przypisywania w kodzie programu jakiegoś zdarzenia, tzw. procedury zdarzeniowej. Procedura zdarzeniowa musi mieć określone parametry, zależne od rodzaju zdarzenia. Przykładowo procedura zdarzeniowa dla zdarzenia OnClick musi mieć parametr Sender typu TObject. Zatem deklaracja takiej procedury będzie wyglądać tak:

private
    procedure MyOnClick(Sedner: TObject);
end;
...
procedure TForm1.Button1Click(Sender: TObject);
var
  MyButton : TButton;
begin
  MyButton := TButton.Create(Form1);
  MyButton.Parent := Form1;
  MyButton.OnClick := MyOnClick; // przypisanie procedury zdarzeniowej
end;
...

W przypadku, gdyby procedura MyOnClick nie posiadała parametru Sender, próba kompilacji programu zakończyłaby się komunikatem o błędzie: [Error] Unit1.pas(32): Incompatible types: 'Parameter lists differ'.

Metody

Przypominam, że metody to inaczej procedury i funkcje zawarte w klasie. Aby owe metody były (w przypadku komponentów) widoczne dla użytkownika komponentu, należy umieścić je w sekcji public.

RTTI

Zajmiemy się teraz pojęciem zwanym RTTI (Run Time Type Information), które umożliwia nam dostęp do informacji na temat właściwości, zdarzeń poszczególnych komponentów ? wszystko podczas działania skompilowanej aplikacji.

Właściwości obiektu

Funkcje, z których będziemy korzystać, znajdują się w pliku TypInfo.pas ? dodaj więc do listy uses moduł TypInfo. W gruncie rzeczy pobieranie listy właściwości danego obiektu jest proste ? wystarczy skorzystać z funkcji GetPropList. W rzeczywistości okazuje się, że czeka nas wiele pracy z wskaźnikami.

Funkcja GetPropList w module TypInfo jest zadeklarowana następująco:

function GetPropList(TypeInfo: PTypeInfo; TypeKinds: TTypeKinds; PropList: PPropList): Integer;

Pierwszy parametr tej funkcji może zawierać wskazanie komponentu, którego będzie dotyczyć operacja ? w to miejsce możemy np. wstawić instrukcję Button1.ClassInfo. Drugi parametr jest pewnego rodzaju filtrem. Określa, jakie właściwości chcemy wyświetlić. Jest to właściwość typu zbiorowego, zadeklarowana następująco:

type
  TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
    tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
    tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);
  TTypeKinds = set of TTypeKind;

Możemy podać, jakie właściwości chcemy wyświetlić. Najlepszym rozwiązaniem jest wstawienie jako drugiego parametru słowa tkProperties, które obejmuje wszystkie właściwości. Podsumowując, wywołanie funkcji GetPropList może wyglądać tak:

    GetPropList(btnGet.ClassInfo, tkProperties, PropList);

Zakładamy, że ostatni parametr ? PropList ? jest typu PPropList. Z kolei typ PPropList jest zadeklarowany następująco:

  PPropList = ^TPropList;
  TPropList = array[0..16379] of PPropInfo;

A zatem okazuje się, że PPropList jest wskazaniem typu TPropList, który z kolei jest tablicą PPropInfo:

PPropInfo = ^TPropInfo;
  TPropInfo = packed record
    PropType: PPTypeInfo;
    GetProc: Pointer;
    SetProc: Pointer;
    StoredProc: Pointer;
    Index: Integer;
    Default: Longint;
    NameIndex: SmallInt;
    Name: ShortString;
  end;

Wiem, wiem, że to wszystko jest zagmatwane ? jeden typ jest wskazaniem drugiego typu itp., ale musisz się przyzwyczaić, że całe VCL w ten właśnie sposób jest zbudowane. Pełny kod programu został przedstawiony w listingu 14.1, a program w trakcie działania ? na rysunku 14.6.

Listing 14.1. Pełny kod źródłowy modułu

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Grids, ValEdit;

type
  TMainForm = class(TForm)
    btnGet: TButton;
    vleValue: TValueListEditor;
    procedure btnGetClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses TypInfo;

procedure TMainForm.btnGetClick(Sender: TObject);
var
  PropList : PPropList;
  i : Integer;
begin
  GetMem(PropList, SizeOf(PropList^)); // zarezerwuj pamięć
  try
  // pobierz listę właściwości
    GetPropList(btnGet.ClassInfo, tkProperties, PropList);
    for I := 0 to High(PropList^) ?1 do
    begin
      if (PropList^[i] <> nil) then
        vleValue.InsertRow(PropList^[i].Name, PropList^[i].PropType^.Name, True)
    end;
  finally
    FreeMem(PropList);
  end;
end;

end.

14.6.jpg
Rysunek 14.6. Właściwości komponentu TButton

Wywołując funkcję GetPropList w ten sposób:

GetPropList(btnGet.ClassInfo, tkProperties + [tkMethod], PropList);

spowodujemy, że pod uwagę zostaną wzięte także metody danego obiektu.

Dokładniejsze informacje o obiekcie

W poprzednim podpunkcie omówiłem zagadnienie związane z pobieraniem informacji na temat właściwości danego obiektu. Tym razem zajmiemy się pobraniem informacji na temat samego obiektu (nazwa klasy, nazwa modułu itp.). Możemy to zrobić za pomocą funkcji GetTypeData, która zdefiniowana jest następująco:

function GetTypeData(TypeInfo: PTypeInfo): PTypeData;

Funkcję można wywołać np. w ten sposób:

  ClassInfo := GetTypeData(btnGet.ClassInfo);

Dzięki temu rekord ClassInfo będzie zawierał informacje na temat komponentu btnGet (typu TButton). Rekord ClassInfo jest rekordem typu PTypeData, wyglądającym następująco:

PTypeData = ^TTypeData;
  TTypeData = packed record
    case TTypeKind of
      tkUnknown, tkLString, tkWString, tkVariant: ();
      tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (
        OrdType: TOrdType;
        case TTypeKind of
          tkInteger, tkChar, tkEnumeration, tkWChar: (
            MinValue: Longint;
            MaxValue: Longint;
            case TTypeKind of
              tkInteger, tkChar, tkWChar: ();
              tkEnumeration: (
                BaseType: PPTypeInfo;
                NameList: ShortStringBase));
          tkSet: (
            CompType: PPTypeInfo));
      tkFloat: (
        FloatType: TFloatType);
      tkString: (
        MaxLength: Byte);
      tkClass: (
        ClassType: TClass;
        ParentInfo: PPTypeInfo;
        PropCount: SmallInt;
        UnitName: ShortStringBase;
       {PropData: TPropData});
      tkMethod: (
        MethodKind: TMethodKind;
        ParamCount: Byte;
        ParamList: array[0..1023] of Char
       {ParamList: array[1..ParamCount] of
          record
            Flags: TParamFlags;
            ParamName: ShortString;
            TypeName: ShortString;
          end;
        ResultType: ShortString});
      tkInterface: (
        IntfParent : PPTypeInfo; { ancestor }
        IntfFlags : TIntfFlagsBase;
        Guid : TGUID;
        IntfUnit : ShortStringBase;
       {PropData: TPropData});
      tkInt64: (
        MinInt64Value, MaxInt64Value: Int64);
  end;

Program wykorzystujący powyższy rekord przedstawiony jest na rysunku 14.7. Sam kod źródłowy znajduje się w listingu 14.2.

14.7.jpg
Rysunek 14.7. Informacje na temat obiektu TButton

Listing 14.2. Kod źródłowy modułu

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Grids, ValEdit;

type
  TMainForm = class(TForm)
    btnGet: TButton;
    vleValues: TValueListEditor;
    procedure btnGetClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses TypInfo;

procedure TMainForm.btnGetClick(Sender: TObject);
var
  ClassInfo : PTypeData;
begin
  ClassInfo := GetTypeData(btnGet.ClassInfo);

  vleValues.InsertRow('Nazwa klasy', ClassInfo.ClassType.ClassName, True);
  vleValues.InsertRow('Klasa bazowa', ClassInfo.ClassType.ClassParent.ClassName, True);
  vleValues.InsertRow('Moduł', ClassInfo.UnitName, True);
  vleValues.InsertRow('Liczba właściwości', IntToStr(ClassInfo.PropCount), True);
  vleValues.InsertRow('Liczba parametrów', IntToStr(ClassInfo.ParamCount), True);
  vleValues.InsertRow('Rozmiar', IntToStr(btnGet.InstanceSize), True);
end;

end.

Podsumowanie

Niniejszy rozdział poświęcony był bardziej teoretycznym zagadnieniom, związanym z komponentami i z biblioteką VCL. Przybliżyłem w nim budowę samej biblioteki VCL, wspomniałem też o podstawowych klasach, na których opiera się owa biblioteka. Nie zapomniałem także o CLX. Praktycznym tworzeniem komponentów w Delphi zajmiemy się w kolejnym rozdziale.

Załączniki:

de25kp.jpg Więcej informacji

Delphi 2005. Kompendium programisty
Adam Boduch
Format: B5, stron: 1048
oprawa twarda
Zawiera CD-ROM
[[Delphi/Kompendium|Spis treści]]

[[Delphi/Kompendium/Prawa autorskie|©]] Helion 2003. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

3 komentarzy

"AutoSize Określa wyrównanie komponentu względem obiektu-rodzica"
Hehe..dobre :D

Pojawiło się mnóstwo '?' w kodzie, warto by go poprawić.

mam kłopot z funkcją FindComponent.
zrobiłem sobie nową klasę w innym unicie ( jak w tym tekście o klasach w kompendium ).
dodałem wszystkie potrzebne unity w USES - między innymi Classes, a i tak nie rozpoznaje funkcji FindComponent :?
Undeclared identifier: 'FindComponent'.
Dziwne jest to, że w głównym module ( ten z formami ), ta funkcja jest rozpoznawalna.
POMOCY!!!