R07-03.DOC

(451 KB) Pobierz
Szablon dla tlumaczy

1

 

Rozdział 7.               Tworzenie własnych komponentów

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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 źró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ądź 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ądź 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.

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 źródłowego. Zmieniając nazwę tworzonej klasy – w tym przypadku na TStyleLabel – spowodujemy automatyczną zmianę proponowanej nazwy modułu źródłowego.

Gdy klikniemy przycisk OK, C++Builder dokona automatycznego wygenerowania modułu źródłowego związanego z tworzonym komponentem (patrz wydruki 7.1 i 7.2). Jedyną niezbędną z 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 <SysUtils.hpp>

#include <Controls.hpp>

#include <Classes.hpp>

#include <Forms.hpp>

#include <StdCtrls.hpp>

//---------------------------------------------------------------------------

class PACKAGE TStyleLabel : public TLabel

{

private:

protected:

public:

        __fastcall TStyleLabel(TComponent* Owner);

__published:

};

//---------------------------------------------------------------------------

#endif

 

Wydruk 7.1. StyleLabel.cpp – wygenerowany tekst modułu źródłowego dla nowo tworzonego komponentu

 

//---------------------------------------------------------------------------

 

#include <vcl.h>

#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)

{

 

     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ą wyobraźni 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 źró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 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ą wyobraźnia.

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 źró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 źró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);

}

 

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;

 

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();

 

 

Podobnie przypisanie do właściwości nowej wartości jest tylko symboliczn...

Zgłoś jeśli naruszono regulamin