Wykład XVII
Object Pascal – Tworzenie
komponentów
Łatwość tworzenia nowych
komponentów i ich adaptacji jest
jednym z największych atutów Delphi.
Podstawy informatyki
Semestr II Transport
Kiedy tworzenie nowego
komponentu jest
uzasadnione ?
jeżeli zastosowanie tworzonego komponentu nie
ograniczy się do jednej aplikacji to praca włożona w jego
tworzenie zwraca się wielokrotnie
stworzenie nowych komponentów może przyczynić się do
bardziej przejrzystego i przyjaznego dla użytkownika
sposobu aplikacji, gdyż logiczny podział jej funkcji (z
punktu widzenia użytkownika) znajdzie odzwierciedlenie
w strukturze użytych komponentów
dla nietypowych zastosowań, gdy brak w środowisku
odpowiednich komponentów – stworzenie ich staje się
warunkiem koniecznym dla stworzenia aplikacji
sprzedaż uniwersalnych komponentów innym
projektantom aplikacji
może stanowić wyzwanie dla projektanta, być
sprawdzianem jego umiejętności
Etapy tworzenia nowego
komponentu
Szczegółowe określenie funkcji komponentu
Zaprojektowanie algorytmu działania komponentu
Sprecyzowanie oraz hierarchiczne i chronologiczne
uporządkowanie poszczególnych czynności
projektowych
Tworzenie komponentu (kodowanie) – powinno
odzwierciedlać logikę komponentu
Testowanie komponentu w ramach projektów
testowych
Zainstalowanie komponentu na palecie
komponentów
Programowe etapy tworzenia
komponentów
Wybór klasy bazowej
Stworzenie modułu źródłowego komponentu
Uzupełnienie definicji dziedziczonych z klasy
bazowej o charakterystyczne dla nowego
właściwości, metody i zdarzenia
Testowanie komponentu
Rejestracja komponentu w środowisku
programistycznym
Stworzenie plików pomocy opisującego
działanie i sposób korzystania z komponentu
Wybór klasy bazowej
Właściwy wybór klasy bazowej ma
decydujący wpływ na postać finalnego
komponentu i wymagany do jego powstania
nakład pracy.
Należy starać się dobrać klasę bazową tak,
by w jak największym stopniu wykorzystać
istniejące właściwości i metody.
Nie ma gotowej recepty na właściwy wybór
jest od wynikiem wiedzy, umiejętności i
doświadczenia programisty, czasem może
być nawet polem eksperymentów.
Tworzenie modułu
źródłowego
Nieodłącznym elementem każdego komponentu jest jego definicja w
kodzie źródłowym, domyślnie każdemu komponentowi odpowiada
oddzielny moduł źródłowy (unit). Definiowanie nowego komponentu
rozpoczyna się od wybrania opcji File|New|Component –
wyświetlenia eksperta komponentów. Umożliwia on wybór klasy
bazowej, określenie nazwy tworzonej klasy i jego modułu źródłowego,
wybór strony na palecie komponentów na której komponent zostanie
umieszczony. W rezultacie powstanie jedynie szkielet modułu
zawierający definicję klasy i procedurę rejestracyjną.
Delph
i 4
Delph
i 2
Definiowanie właściwości
prostych
TPrzykladowaKlasa=class(TCustomControl)
private
FIntegerProp: Interer;
FStringProp: String;
FCharProp: Char;
published
property IntererProp read FIntegerProp write
FIntegerProp;
property StringProp read FStringProp write
FStringProp;
property CharProp read FCharProp write FCharProp;
end;
Właściwości reprezentują liczby, wartości lub znaki
Definiowanie właściwości
wyliczeniowych
TPoryRoku=(prWiosna,prLato,prJesien,prZima);
TPrzykladowaKlasa=class(TCustomControl)
private
{pola wewnętrzne}
FPoryRoku: TPoryRoku;
FBooleanProp: Boolean;
published
{właściwości wyliczeniowe}
property PoryRoku read FPoryRoku write FPoryRoku;
property BooleanProp read FBooleanProp write
FBooleanProp;
end;
Wymaga zwykle uprzedniego zdefiniowania typu
wyliczeniowego, edycja w Object Inspectorze
pozwala wybierać wartość właściwości z listy
Definiowanie właściwości zbiorowych
TDni=(Poniedziałek,Wtorek,Środa,Czwartek,Piątek,Sobota,Nie
dziela);
TSetDni=Set of TDni;
TPrzykladowaKlasa=class(TCustomControl)
private
{pola wewnętrzne}
FDyzury: TSetDni;
published
{właściwości wyliczeniowe}
property Dyzury read FDyzury write FDyzury;
end;
Należy zdefiniować bazowy typ wyliczeniowy i oparty na nim
typ zbiorowy, a następnie można zdefiniować odpowiednią
właściwość zbiorową.
Definiowanie właściwości
obiektowych
Nic nie stoi na przeszkodzie,
by właściwością klasy, była
sama klasą lub komponentem.
Za przykład można podać
właściwość Font.
Definicja obiektu, który ma być właściwością innej
klasy powinna (bezpośrednio lub pośrednio)
wywodzić się z klasy TPersistent. Kreowanie
egzemplarza obiektu zawierającego właściwości
obiektowe musi tworzyć obiekt związany z
właściwością obiektową.Podobnie należy
postępować podczas niszczenia obiektu – należy
niszczyć obiekty podrzędne przed zniszczeniem
obiektu nadrzędnego. Konieczne jest zatem
przedefiniowanie konstruktora i destruktora.
Przykładowy obiekt
podrzędny
TJakisObiekt=class(TPersistent)
private
FProp1: Integer;
FProp2: String;
published
property Prop1: Integer read FProp1 write
FProp1;
property Prop2: String read FProp2 write
FProp2;
end;
Przykład definiowania
właściwości obiektowych
TObiektNadrzedny=class(TCustomControl)
private
FJakisObiekt: TJakisObiekt;
procedure SetJakisObiekt(Value: TJakisObiekt);
public
constructor Create(AOwner: TComponent);override;
destructor Destroy;override;
published
property JakisObiekt: TJakisObiekt read FJakisObiekt
write SetJakisObiekt;
end;
Tworzenie obiektów
wewnętrznych
obiekt podrzędny tworzymy w konstruktorze
po utworzeniu obiektu rodzica
constructor TObiektNadrzedny.Create(AOwner:
TComponent);
begin
inherited Create(Aowner);
FJakisObiekt:=TJakisObiekt.Create;
end;
Zwalnianie obiektów
wewnętrznych
obiekt dziecko niszczymy przed zwolnieniem
obiektu nadrzędnego w destruktorze
destructor TObiektNadrzedny.Destroy;
begin
FJakisObiekt.Destroy;
inherited Destroy;
end;
Przypisanie wartości
własności obiektowej
Należy sprawdzić czy przypisany jest jakiś
egzemplarz obiektu. Jeśli tak należy go zwolnić i
dopiero wówczas przypisać nową wartość
właściwości obiektowej
procedure TObiektNadrzedny.SetJakisObiekt(Value:
TJakisObiekt);
begin
if Assigned(FJakisObiekt) then FJakisObiekt.Free;
if Assigned(Value) then FJakisObiekt.Assign(Value)
end;
Redefinicja metody
Assign
Należy tu powiedzieć, że należałoby przedefiniować
metodę Assign obiektu dziecka tak aby obsługiwała
nowe właściwości obiektu
procedure TJakisObiekt.Assign(Source: TPersistant);
begin
if Source is TJakisObiekt then
begin
FProp1:=TJakisObiekt(Source).Prop1;
FProp2:=TJakisObiekt(Source).Prop2;
inherited Assign(Source);
end;
end;
Definiowanie właściwości
tablicowych
Jeśli chcielibyśmy je umieścić w Object
Inspektorze to należy zdefiniować specjalny
edytor właściwości
Właściwość tablicowa musi posiadać jeden lub
więcej indeksów (mogą być one dowolnego
typu)
Klauzule read i write w definicji właściwości
muszą określać metody dostępowe – nie jest
dozwolone specyfikowanie pól
Liczba i kolejność indeksów w odwołaniu do
właściwości być zgodna z jej deklaracją
Przykład typu z
właściwościami
tablicowymi
TPlanety=class(TComponent)
private
function GetPlanetName(const AIndex: Integer): String;
function GetPlanetPosition(const APlanetName: String):
Integer;
public
property PlanetName[const AIndex: Integer]: String
read GetPlanetName; default;
property PlanetPosition[const APlanetName: String]:
Integer
read GetPlanetPosition;
end;
Właściwości tablicowe -
Metody dostępowe
-
Przykład
function TPlanety.GetPlanetName
(const AIndex: Integer): String;
begin
if (AIndex<=0) or (AIndex>9) then
raise Exception.Create(‘Niepoprawny
numer planety. ‘+
’Wprowadź liczbę od 1 do 9’)
else Result::=PlanetNames[AIndex];
end;
Właściwości tablicowe -
Metody dostępowe –
Przykład
cd.
function TPlanety.GetPlanet Position
(const APlanetName: String): Integer;
var I: Integer;
begin
Result:=0; i:=0;
repeat
inc(i);
until (i>9) or
(ANSICompareStr(ANSIUpperCase(APlanetName),
ANSIUpperCase(PlanetNames[i]))=0);
if i<=9 then Result:=i;
end;
Metody dostępowe -
zmiana wartości właściwości
TJakisObiekt=class(TComponent)
{...} FWlasnosc:Integer;
procedure SetWlasnosc(Value: Integer);
{...}
property Wlasnosc: Integer read FWlasnosc write
SetWlasnosc;
{...}
procedure TJakisObiekt.SetWlasnosc(Value: Integer);
begin {...}
Wlasnosc:=Value;
{tu nieskończona rekursja – błąd – winno być np.
FWlasnosc:=Value}
{...}
end;
Zdarzenia
Zdarzenie jest wynikiem oddziaływania użytkownika, ,
systemu operacyjnego, bądź samej aplikacji
Reakcją obiektu na wystąpienie zdarzenia jest
wywołanie związanej z nim procedury zdarzeniowej
(event handler);
Każdemu zdarzeniu, na które zdolny jest reagować
obiekt danej klasy, odpowiada określona właściwość
zdarzeniowa (event property). Zapewnia ona dostęp
do pola, będącego wskaźnikiem do określonej
procedury zdarzeniowej.
Definiowanie zdarzeń
TJakasKontrolka=class(TComponent);
private
FOnClick: TNotifyEvent;
protected
procedure Click; dynamic;
property OnClick: TNotifyEvent read FOnClick write
FOnClick;
end;
Procedury zdarzeniowe w zależności od zdarzenia mają
na ogół różny zestaw parametrów. Typ TNotifyEvent
jest zdefiniowany następująco:
TNotifyEvent=procedure(Sender: TObject) of object
Procedura zdarzeniowa
procedure TJakasKontorlka.Click;
begin
if Assigned(FOnClick) then
FOnClick(Self);
end;
procedura sprawdza czy z obsługą
zdarzenia powiązano jakąś procedurę
(Assigned) i jeśli tak wywołuje ją.
Definiowanie metod
Podczas dodawania i przedefiniowywania metod
komponentów posługujemy się schematem przyjętym
dla klas Object Pascala.
Staramy się unikać wzajemnych powiązań między
metodami (unikać wymogu stosowania metody w
towarzystwie innych metod lub co gorsza, w określonej
sekwencji).
Żadna metoda nie powinna wprowadzać obiektu w stan,
w którym pewne zdarzenia lub właściwości nie są
właściwie obsługiwane.
Należy nadawać metodom nazwy sugerujące
wykonywane przez nie akcje
Właściwie dobieramy stopień widoczności metody
Przedefiniowywanie
konstruktorów
wymaga użycia klauzuli override;
constructor Create(AOwner: TComponent);override;
regułą powinno być również wywołanie konstruktora
klasy macierzystej (bazowej). Przedefiniowywany
konstruktor w klasie pochodnej musi mieć taką samą
strukturę parametryczną jak konstruktor oryginalny.
Klasa może mieć jednak kilka konstruktorów.
constructor TMojKomp.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
{...}
end;
Przedefiniowywanie
destruktorów
wymaga również użycia klauzuli override;
destructor Destroy; override;
wewnątrz destruktora wywołanie destruktora klasy
bazowej jest obowiązkowe i powinno być wykonane
jako czynność ostatnia, np.
destructor TMojKomp.Destroy;
begin
FTimer.Free;
MyString.Free;
inherited Destroy;
end;
Zachowanie się
komponentu na etapie
budowania aplikacji
Konstruktor Create(...) komponentu
wywoływany jest nie tylko w czasie wykonywania
programu, lecz również w momencie wstawiania
go do formularza na etapie budowy aplikacji.
Wynika stąd czasem konieczność rozróżnienia
tych dwu etapów. Rozróżnienie jest możliwe
dzięki właściwości zbiorowej ComponentState.
Poszczególne elementy i ich znaczenie
przedstawia tabela
Znaczenie flag właściwości
ComponentState
csAncesto
r
komponent został odziedziczony z formularza
nadrzędnego
csDesigni
ng
aplikacja znajduje się na etapie projektowania
csFixups
do komponentu istnieje odwołanie z formularza,
który nie jest aktualnie załadowany; po
załadowaniu wszystkich formularzy flaga jest
wyłączana
csLoading odczytywanie danych komponentu ze strumienia
csReading flaga jest ustawiana w momencie utworzenia
komponentu (w wyniku odczytu ze strumienia) i
kasowania wówczas, gdy dany komponent oraz
wszystkie komponenty, od których jest on
zależny, mają już ustalone parametry
csUpdatin
g
komponent jest uaktualniany ze względu na
zmianę definicji z formularzu nadrzędnym
csWriting
trwa zapis komponentu do strumienia
Rejestracja komponentu
Rejestracja komponentu jest czynnością, powodującą
umieszczenie go na Palecie komponentów. Wykonuje ją
procedura o nazwie RegisterComponents():
procedure RegisterComponents(const Page: string;
ComponentClasses: array of TComponentClass);
Pierwszy parametr oznacza nazwę palety komponentów
Drugi zawiera nazwy klas rejestrowanych komponentów
Powyższa procedura musi być wywołana w treści
procedury o nazwie Register, zadeklarowanej także w
części publicznej modułu
Przykład rejestrowania
komponentów
unit MojComp;
interface
type TMojKomp=class(TComponent)
{...} end;
TInnyKomp=class(Tcomponent) {...} end;
procedure Register;
implementation{...}
procedure Register;
begin
RegisterComponents(‘MojeKomponenty’,[TMojKomp,
TInnyKomp]);
end;
end.
Testowanie
komponentu
Najczęstszym sposobem testowania
komponentu jest stworzenie projektu w
ramach którego obiekt będzie dynamicznie
tworzony i zwalniany.
Projekt ten powinien również pozwalać na
sprawdzenie wszystkich funkcji komponentu.
Należy pamiętać, że dobrze wykonany
komponent powinien być gotowy do
normalnego funkcjonowania już po
wykonaniu konstruktora.
Wybór ikony
komponentu
Komponent nie będzie kompletny, dopóki nie
zostanie opatrzony ikoną identyfikującą go w
palecie komponentów. Do tworzenia ikon można
wykorzystać Image Editor lub dowolny inny
edytor obsługujący pliki BMP.
Ikona musi mieć rozmiary 24x24 i musi być
przechowywana w pliku o takiej samej nazwie jak
nazwa modułu z komponentem i znajdować się w
tym samym co komponent katalogu.
Format pliku jest pod względem struktury
powinien być zgodny z plikami zasobów Windows
(.RES) lecz powinien mieć rozszerzenie .DCR.