Rozdział 15. Tworzenie komponentów

Adam Boduch

Poprzedni rozdział stanowił wprowadzenie do technik tworzenia komponentów. Omówiłem w nim rolę VCL i CLX w programowaniu w Delphi oraz hierarchię klas. Przyznam, mogło to być nieco nudne ze względu na dużą dawkę wiedzy teoretycznej. Tym razem zajmiemy się już programowaniem własnych projektów i zastosowaniem ich w praktyce.

1 Tworzenie nowego komponentu
2 Edycja kodu
3 Konstruktory i destruktory
4 Właściwości
     4.1 Klauzula default
     4.2 Klauzula stored
     4.3 Klauzula nodefault
     4.4 Właściwość wyliczeniowa
     4.5 Właściwość zbiorowa
5 Zdarzenia
     5.6 Definiowanie własnych zdarzeń
6 Ikona dla komponentów
7 Przykładowy komponent
     7.7 Ogólny zarys klasy
     7.8 Komunikaty
     7.9 Kod źródłowy komponentu
8 Instalacja komponentów
9 Demonstracja możliwości komponentu TURLabel
10 Zachowanie komponentu
11 Komponenty graficzne
     11.10 Ogólny zarys klasy komponentu
     11.11 Kod źródłowy komponentu
12 Pakiety komponentów
13 Podsumowanie

W tym rozdziale:
*wykorzystasz w sposób praktyczny wiedzę na temat komponentów;
*zaprojektujesz swój pierwszy komponent ? TURLabel;
*nauczysz się instalować komponenty;
*zaprojektujesz komponent graficzny.

Tworzenie nowego komponentu

Już dłuższy czas pracujesz z Delphi, powinieneś więc zauważyć istnienie menu Component. Nas na tym etapie zainteresują jedynie dwie pozycje tego menu ? New Component i Install Component. Pierwsze polecenie służy do stworzenia nowego projektu komponentu, natomiast za pomocą drugiego możemy zainstalować już istniejący komponent. Po wybraniu tej opcji na palecie komponentów zostanie utworzona nowa ikonka ? wówczas komponent będzie działał jak zwykły komponent VCL.

Zajmijmy się jednak tworzeniem nowego komponentu. Z menu Component wybierz New Component. Na ekranie zobaczysz wówczas takie okienko, jakie zostało przedstawione na rysunku 15.1.

15.1.jpg
Rysunek 15.1. Tworzenie nowego komponentu

W pierwszym polu z listy rozwijalnej należy wybrać komponent, który będzie stanowił klasę bazową dla naszego nowego obiektu. Ja z tej listy wybrałem klase TLabel. Jeżeli chcesz tworzyć komponent od początku, wybierz pozycję TComponent. Jest to bazowa klasa dla wszystkich komponentów, zawierająca parę potrzebnych czasem procedur i funkcji.

W kolejnym polu ? Class Name ? musisz wpisać nazwę nowego komponentu. Pamiętaj, że wedle obowiązującej zasady należy poprzedzić nazwę literą T. Pole Palette Page określa, na jakiej palecie zostanie zainstalowany komponent. Unit file name to ścieżka do katalogu, w którym zostaną zapisane nowe pliki z komponentem. Zawartość tego pola pozostawiłem niezmienioną. I wreszcie ostatnie pole określa ścieżkę do katalogu, gdzie znajdować się będą potrzebne do kompilacji pliki komponentu. ${DELPHI} oznacza domyślną ścieżkę do pliku programu Delphi.

Po naciśnięciu przycisku Install komponent zostanie zainstalowany na wybranej palecie, natomiast po naciśnięciu przycisku OK w edytorze kodu zostanie jedynie wyświetlona zawartość kodu komponentu.

Edycja kodu

Po wykonaniu operacji opisanych w poprzednim punkcie w edytorze kodu powinien zostać wyświetlony taki kod, jak w listingu 15.1.

Listing 15.1. Podstawowy kod komponentu TURLabel

unit URLabel;

interface

uses
  Windows, Messages, SysUtils, Classes, Controls, StdCtrls;

type
  TURLabel = class(TLabel)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TURLabel]);
end;

end.

Kluczowe znaczenie ma procedura Register. Następuje w niej zarejestrowanie komponentu realizowane przez polecenie RegisterComponents. Pierwszy parametr oznacza nazwę palety. Drugi parametr to nazwy komponentów do zainstalowania. Muszą one być wpisane w nawiasie kwadratowym, gdyż w tym wypadku występują jako elementy tablicy. Taki zapis jest konieczny, gdyż jeden moduł ? w tym wypadku URLabel ? może zawierać kilka klas (komponentów).

Podczas wpisywania zakładki dla tworzonego komponentu nie musisz podawać nazwy zakładki już istniejącej ? jeżeli wpiszesz nazwę nieistniejącej zakładki, zostanie ona utworzona przez Delphi.

Zwróć uwagę, że w klasie możesz używać nowej sekcji ? published. Ta sekcja może zawierać metody, które będą wyświetlone w Inspektorze Obiektów.

Konstruktory i destruktory

Komponenty, podobnie jak zwykłe klasy, posiadają konstruktory oraz destruktory. Nie są to co prawda obowiązkowe elementy klasy, gdyż nawet jeśli ich nie zadeklarujemy, to i tak komponent będzie posiadał domyślny konstruktor z klasy bazowej (najwyżej ? TObject). W przypadku komponentu konstruktor winien mieć parametr AOwner (typu TComponent), który zawierałby wskazanie komponentu (lub formularza) rodzica:

 
public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;

Jak widzisz, zarówno konstruktor, jak i destruktor są opatrzone klauzulą override, co znaczy, że są przedefiniowane (była o tym mowa w 3. rozdziale książki) ? możemy dla nich wpisać kod:

constructor TURLabel.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  { podczas wywoływania konstruktora dla właściwości przypisz domyślny tekst }
  FURL := 'http://4programmers.net';
end;

destructor TURLabel.Destroy;
begin
  inherited;
end;

W tym miejscu oprócz typowego utworzenia obiektu (działanie konstruktora) następuje również przydzielenie wartości dla właściwości FURL.

Właściwości

Właściwości komponentów służą do przechowywania wartości różnych typów. Jednak jeśli chcemy, aby właściwości były widoczne w Inspektorze Obiektów, należy użyć kolejnej sekcji w klasie ? sekcji published:

property URL : String read FURL write FURL;

Właściwość należy oznaczyć słowem kluczowym property. W tym wypadku właściwość URL będzie typu String. Zwykło się także definiować zmienne pomocnicze, których deklaracje umieszcza się w sekcji private. Nazwy owych zmiennych umieszczane są po słowach kluczowych read ``i write. W ten sposób łatwo utworzyć jakąś właściwość tylko do odczytu (pozostawiając jedynie klauzulę read) lub tylko do zapisu (klauzula write), albo też zarówno do zapisu, jak i do odczytu (zarówno read, jak i write).

Regułą stało się już specjalne nazewnictwo zmiennych pomocniczych, polegające na dodawaniu przed nazwą litery F.

Klauzula default

Podczas pisania samego komponentu istnieje możliwość określania domyślnej wartości za pomocą klauzuli default.

property Count : Integer read FCount write FCount default 1;

W takim wypadku domyślna wartość właściwości Count to 1.

Klauzula default obejmuje jedynie typy liczbowe i zbiory (set) ? nie obsługuje natomiast łańcuchów String.

Klauzula stored

W przypadku typów typu Boolean używa się klauzuli stored, a nie default ? np.:

property DoIt : Boolean read FDoIt write FDoIt stored False;

Jeśli nie skorzystamy z klauzuli stored, program za domyślną wartość dla właściwości przyjmie True.

Klauzula nodefault

Klauzula nodefault jest przeciwieństwem defulat ? oznacza, że właściwość nie będzie miała wartości domyślnej. Stosowana jest jedynie w niektórych przypadkach, gdy klasa bazowa, z której korzysta nasz komponent, posiada właściwość domyślną.

TBaseClass = class
private
  FProp : Integer;
published
  property Prop : Integer read FProp write FProp default 1;
end;

TMyClass = class(TBaseClass)
private
  FProp : Integer;
published
  property Prop : Integer read FProp write FProp nodefault;
end;

W takim wypadku klasa TMyClass także będzie posiadać właściwość Prop, tyle że nie będzie już ona zawierała wartości domyślnej.

Właściwość wyliczeniowa

O właściwościach wyliczeniowych wspominałem w poprzednim rozdziale, lecz tym razem zajmiemy się ich deklaracją. Przypominam, że właściwość wyliczeniowa to lista rozwijalna z możliwymi wartościami (rysunek 15.2).

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

W języku Object Pascal właściwości wyliczeniowe to w rzeczywistości deklaracje typu set of.

type
  TSetCarOption = (coFiat, coOpel, coPorshe);
  TSetCarOptions = set of TSetCarOption;

TMyClass = class(TComponent)
  private
    FCar : TSetCarOptions;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    property Car : TSetCarOptions read FCar write FCar default [coFiat];
  end;

Zadeklarowaliśmy właśnie właściwość wyliczeniową typu TSetCarOptions. W tym wypadku właściwość Car będzie posiadała domyślną wartość ? coFiat.

Właściwość zbiorowa

Przykład właściwości zbiorowej przedstawiony został na rysunku 15.3.

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

Deklarowanie właściwości zbiorowej w rzeczywistości wiąże się z zadeklarowaniem nowego typu danych (klasy).

type
  TCarClass = class(TPersistent)
  private
    FFuel : Integer;
    FCarName : String;
  published
    property CarName : String read FCarName write FCarName;
    property Fuel : Integer read FFuel write FFuel;
  end;

  TMyClass = class(TComponent)
  private
    FCar : TCarClass;
  protected
    { Protected declarations }
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  published
    property Car : TCarClass read FCar write FCar;
  end;

Nasz komponent będzie posiadał właściwość zbiorową Car. Po jej rozwinięciu w Inspektorze Obiektów pojawią się właściwości z klasy TCarClass. Mamy do czynienia z nową klasą TCarClass i przed skorzystaniem z jej właściwości należy ją utworzyć (wywołać konstruktor):

constructor TMyClass.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FCar := TCarClass.Create; // utworzenie klasy
end;

destructor TMyClass.Destroy;
begin
  FCar.Free; // zwolnienie
  inherited;
end;

Zdarzenia

Zdarzenia, podobnie jak właściwości naszego komponentu, muszą być deklarowane w sekcji published ? tak, aby były widoczne w Inspektorze Obiektów ? np. w ten sposób:

property OnMouseEnter : TNotifyEvent read FOnMouseEnter write FOnMouseEnter;
property OnMouseLeave : TNotifyEvent read FOnMouseLeave write FOnMouseLeave;

Zapis ten jest dość dziwny, gdyż sprawia wrażenie, jakbyśmy deklarowali zwykłe właściwości, tyle że typu TNotifyEvent. Typ TNotifyEvent jest zadeklarowany w module Classes:

type TNotifyEvent = procedure (Sender: TObject) of object;

Można powiedzieć, że określa on procedurę zdarzeniową, która miałaby tylko jeden parametr ? Sender. Już nieraz podczas lektury tej książki miałeś okazję zapoznać się z moją opinią o tym, że dana procedura zdarzeniowa musi mieć parametr Sender. Teraz już wiesz, że zdarzenie OnClick jest typu TNotifyEvent i stąd musi posiadać parametr Sender.

Definiowanie własnych zdarzeń

Czasem może przytrafić się sytuacja, gdy w naszym komponencie będzie wymagane zdarzenie zawierające więcej niż jeden parametr ? po prostu będzie umożliwiało użytkownikowi przekazanie jakiś funkcji. W takim wypadku konieczne stanie się zadeklarowanie nowego typu zdarzenia:

type
  TMyEvent = procedure(Sender: TObject; X : Integer) of object;

Taka składnia ? tj. umieszczenie frazy of object na końcu ? jest obowiązkowa. Teraz oprócz zwykłego parametru Sender zdarzenie będzie posiadać także parametr X. Co z nim zrobimy? To już zależy od programisty.

Gdy chcesz, aby jakieś działanie komponentu spowodowało wystąpienie zdarzenia, możesz zastosować po prostu taki kod:

OnURLClick(Self, 1);

A zatem pierwszym parametrem Self zastępujemy wymagany parametr typu TObject, natomiast drugi parametr przekazany wraz ze zdarzeniem to cyfra 1.

Ikona dla komponentów

Po zainstalowaniu komponentu Delphi przydzieli dla niego swoją własną ikonę. W każdym wypadku można to zmienić, przypisując temu komponentowi osobną ikonę. Wystarczy skorzystać z programu do tworzenia bitmap. Możesz użyć np. programu Image Editor, którego już wcześniej używaliśmy przy okazji tworzenia różnych zasobów.

W przypadku, gdy tworzysz zasoby dla komponentu, tworzysz plik .DCR, a nie .RES! Uważaj na to! Także słowo ikona komponentu jest tylko terminem umownym, gdyż w zasobie musisz stworzyć NIE ikonę, ale BITMAPĘ o rozmiarach 24×24 piksele.

Stwórz więc nowy plik .DCR, a w nim bitmapę 24´24 piksele, na której narysuj jakiś obrazek. Cały zasób następnie włącz do kodu komponentu za pomocą dyrektywy {$R ZASOBY.DCR}.

Należy pamiętać o jeszcze jednej kwestii związanej z nazewnictwem bitmapy ? musi się ona nazywać tak samo, jak klasa (komponent), która wykorzystuje tę ikonę. A zatem jeżeli komponent nazywa się TURLabel, bitmapa musi również nosić nazwę TURLABEL.

Przykładowy komponent

Właściwie możesz już przystąpić do napisania swojego pierwszego komponentu. Nie będzie to nic nadzwyczajnego ? po prostu etykieta, która jest w rzeczywistości odnośnikiem do strony WWW. Jej kliknięcie spowoduje otwarcie żądanego adresu.

Ogólny zarys klasy

W przypadku, gdy masz już otwarty nowy projekt, powinieneś mieć w edytorze ?czysty? kod tworzonego właśnie komponentu. Kod naszej nowej klasy TURLabel powinien przedstawiać się w następujący sposób:

TURLabel = class(TLabel)
  private
    FURL : String;
    FParametr : String;
    FOnMouseEnter, FOnMouseLeave : TNotifyEvent;
    FOnURLClick : TClickEvent;
    FDefaultFontColor : TColor;
    FDefaultFontStyle : TFontStyles;
  protected
    procedure CmMouseEnter(var Msg : TMessage); message CM_MOUSEENTER;
    procedure CmMouseLeave(var Msg : TMessage); message CM_MOUSELEAVE;
    procedure WMLButtonDown(var Msg : TMessage); message WM_LBUTTONDOWN;
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  published
    property URL : String read FURL write FURL; // URL do otwarcia
    property Parametr : String read FParametr write FParametr; // dodatkowy parametr
    property OnMouseEnter : TNotifyEvent read FOnMouseEnter write FOnMouseEnter;
    property OnMouseLeave : TNotifyEvent read FOnMouseLeave write FOnMouseLeave;

    property OnURLClick : TClickEvent read FOnURLClick write FOnURLClick;
  end;

Komponent będzie posiadał standardową właściwość URL, określającą adres strony, która ma zostać otwarta. Zdarzenia OnMouseEnter oraz OnMouseLeave będą występowały w momencie umieszczenia kursora myszy nad obiektem lub przemieszczenia kursora znad obiektu. Dodatkowe zdarzenie OnURLClick wystąpi, gdy użytkownik kliknie odnośnik. Będzie ono przekazywało do programu współrzędne pozycji kursora X i Y, pobrane w momencie naciśnięcia przycisku myszy.

Komunikaty

Do przechwycenia momentu umieszczenia kursora nad obiektem niezbędne będzie skorzystanie z komunikatów, a ściślej mówiąc z CM_MOUSEENTER oraz CM_MOUSELEAVE.

procedure TURLabel.CMMouseEnter(var Msg : TMessage);
begin
{ jeżeli wykorzystane jest zdarzenie FOnMouseEnter ? wywołaj je }
  if Assigned(FOnMouseEnter) then OnMouseEnter(Self);
  FDefaultFontColor := Font.Color; // pobierz do zmiennej kolor czcionki
  FDefaultFontStyle := Font.Style;  // pobierz styl czcionki (pogrubiony, podkreślony)

  Cursor := crHandPoint;  // zmień kursor
  Font.Color := clBlue;  // zmień kolor czcionki
  Font.Style := Font.Style + [fsUnderline]; // dodaj podkreślenie
end;

procedure TURLabel.CmMouseLeave(var Msg : TMessage);
begin
{ jeżeli wykorzystane jest zdarzenie FOnMousLeave ? wywołaj je }
  if Assigned(FOnMouseLeave) then OnMouseLeave(Self);

  { przywróć zapisane w zmiennej dane }
  Font.Color := FDefaultFontColor;
  Font.Style := FDefaultFontStyle;
end;

Na samym początku po wykryciu wystąpienia komunikatu generowane są zdarzenia OnMouseEnter oraz OnMouseLeave:

if Assigned(FOnMouseEnter) then OnMouseEnter(Self);

Od tego momentu obsługą zdarzenia musi się zająć użytkownik komponentu. Oprócz tego umieszczenie kursora myszy w obszarze komponentu spowoduje zmianę koloru czcionki na niebieski oraz dodanie podkreślenia. Do wykonania tego zadania niezbędne było wcześniejsze zadeklarowanie pól FDefaultFontColor oraz FDefaultFontStyle. Po odsunięciu kursora znad obiektu przywrócone zostaną domyślne ustawienia koloru oraz kroju czcionki.

Za pomocą metody Assigned na początku dokonuje się sprawdzenia, czy użytkownik komponentu wygenerował owe zdarzenie; jeżeli tak się stało, jest ono generowane.

Kod źródłowy komponentu

Pełny kod źródłowy komponentu TURLabel został przedstawiony w listingu 15.2. Zwróć uwagę na obecność dyrektywy {$R ZASOBY.DCR}, za pomocą której włączane są zasoby, co w konsekwencji prowadzi do zmiany ikony komponentu.

Listing 15.2. Kod źródłowy komponentu TURLabel

{
   Copyright (c) 2002 by Adam Boduch 2002
}

unit URLabel;

interface

uses
  Windows, Messages, SysUtils, Graphics, Classes, Dialogs, Controls, StdCtrls;

  {$R ZASOBY.DCR}

type
  TClickEvent = procedure(Sender: TObject; X, Y : Integer) of object;
  
  TURLabel = class(TLabel)
  private
    FURL : String;
    FParametr : String;
    FOnMouseEnter, FOnMouseLeave : TNotifyEvent;
    FOnURLClick : TClickEvent;
    FDefaultFontColor : TColor;
    FDefaultFontStyle : TFontStyles;
  protected
    procedure CmMouseEnter(var Msg : TMessage); message CM_MOUSEENTER;
    procedure CmMouseLeave(var Msg : TMessage); message CM_MOUSELEAVE;
    procedure WMLButtonDown(var Msg : TMessage); message WM_LBUTTONDOWN;
  public
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  published
    property URL : String read FURL write FURL; // URL do otwarcia
    property Parametr : String read FParametr write FParametr; // dodatkowy parametr
    property OnMouseEnter : TNotifyEvent read FOnMouseEnter write FOnMouseEnter;
    property OnMouseLeave : TNotifyEvent read FOnMouseLeave write FOnMouseLeave;

    property OnURLClick : TClickEvent read FOnURLClick write FOnURLClick;
  end;

procedure Register;

implementation

uses ShellAPI;

constructor TURLabel.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  { podczas wywoływania konstruktora dla właściwości przypisz domyślny tekst }
  FURL := 'http://4programmers.net';
end;

destructor TURLabel.Destroy;
begin
  inherited;
end;

procedure TURLabel.WMLButtonDown(var Msg : TMessage);
var
  P : TPoint;
begin
{ w razie kliknięcia komponentu otwórz program lub stronę WWW, której ścieżka
  jest wpisana we właściwości FURL }
  GetCursorPos(P); // pobranie współrzędnych kursora myszy  
  ShellExecute(0, 'open', PChar(FURL), PChar(FParametr), nil, SW_SHOW);
  if Assigned(FOnURLClick) then OnURLClick(Self, P.X, P.Y); // generowanie zdarzenia
end;

procedure TURLabel.CMMouseEnter(var Msg : TMessage);
begin
{ jeżeli wykorzystane jest zdarzenie FOnMouseEnter ? wywołaj je }
  if Assigned(FOnMouseEnter) then OnMouseEnter(Self);
  FDefaultFontColor := Font.Color; // pobierz do zmiennej kolor czcionki
  FDefaultFontStyle := Font.Style;  // pobierz styl czcionki (pogrubiony, podkreślony)

  Cursor := crHandPoint;  // zmień kursor
  Font.Color := clBlue;  // zmień kolor czcionki
  Font.Style := Font.Style + [fsUnderline]; // dodaj podkreślenie
end;

procedure TURLabel.CmMouseLeave(var Msg : TMessage);
begin
{ jeżeli wykorzystane jest zdarzenie FOnMousLeave ? wywołaj je }
  if Assigned(FOnMouseLeave) then OnMouseLeave(Self);

  { przywróć zapisane w zmiennej dane }
  Font.Color := FDefaultFontColor;
  Font.Style := FDefaultFontStyle;
end;

procedure Register;
begin
  RegisterComponents('Samples', [TURLabel]);
end;

end.

Instalacja komponentów

Gdy mamy już gotowy komponent, możemy go zainstalować. wybierając z menu Component polecenie Install Component.

Po wykonaniu tej czynności powinieneś na ekranie ujrzeć okienko przedstawione na rysunku 15.4. Nas w tej chwili interesuje tylko pierwsza zakładka. Pierwsze pole musi zawierać ścieżkę do pliku źródłowego komponentu. Po naciśnięciu przycisku Browse możesz wskazać ten plik. To właściwie wszystko.

15.4.jpg
Rysunek 15.4. Okno wyboru komponentu

Pozycja Package file name zawiera nazwę pakietu, do którego zostanie dodany nasz komponent. W tym miejscu powinna znajdować się ścieżka domyślnego pakietu Borlanda. Na tym etapie nie musisz nic zmieniać ? tworzeniem własnych pakietów zajmiemy się nieco później.

Naciśnij przycisk OK w celu kontynuowania instalacji. Wyświetlona zostanie zawartość pakietu dclusr.dpk Okno powinno wyglądać tak, jak na rysunku 15.5.

15.5.jpg
Rysunek 15.5. Instalacja komponentu w pakiecie dclusr.dpk

W oknie tym znajduje się lista komponentów należących do tego pakietu. Za pomocą przycisku Add możesz do tego pakietu dodać kolejny komponent. Przyciskiem Remove możesz natomiast usunąć komponent z pakietu i tym samym go odinstalować.

Aby sfinalizować proces instalacji, naciśnij przycisk Compile. Po jego naciśnięciu Delphi spróbuje skompilować moduł zawierający komponent do postaci .dcu i w końcu dodać ten komponent do palety komponentów na wybranej zakładce. Jeżeli wszystko pójdzie dobrze, zobaczysz okienko informacyjne z komunikatem dotyczącym prawidłowej instalacji komponentu. W razie wystąpienia błędów kompilator otworzy zawartość modułu i wskaże, gdzie nastąpił błąd podczas kompilacji.

Jeżeli tworzysz komponenty na potrzeby wielu użytkowników, czyli rozpowszechniasz dany komponent np. w Internecie, nie musisz dołączać pliku źródłowego komponentu ? wystarczy jedynie forma skompilowana (plik .dcu). Ale uwaga! Jeżeli Ty używasz do tworzenia komponentu wersji 7 Delphi i nie udostępniasz jego kodu źródłowego, a jedynie formę skompilowaną, to osoba chcąca zainstalować ten komponent na swoim komputerze także będzie musiała posiadać wersję 7. W przeciwnym wypadku proces instalacji się nie powiedzie.

Demonstracja możliwości komponentu TURLabel

Stworzymy teraz program demonstrujący funkcjonalność komponentu TURLabel. Pisząc komponenty i udostępniając je szerszej publiczności, powinieneś zawsze dołączać wersję próbną swojego komponentu, tak aby użytkownik jak najszybciej mógł sprawdzić jego działanie. Moja propozycja programu prezentacyjnego jest przedstawiona na rysunku 15.6.

15.6.jpg
Rysunek 15.6. Program prezentujący działanie komponentu TURLabel

Po instalacji komponent powinien zostać umieszczony na palecie Samples. Umieść naszą nową kontrolkę na formularzu. Spójrz na zakładkę Events z Inspektora Obiektów ? znajdują się tam zadeklarowane przez nas zdarzenia (rysunek 15.7).

15.7.jpg
Rysunek 15.7. Zdarzenie OnURLClick

Po wygenerowaniu zdarzenia OnURLClick zauważysz, że procedura zdarzeniowa posiada parametry X i Y:

procedure TDemoForm.URLabel1URLClick(Sender: TObject; X, Y: Integer);
begin
  lblTip.Caption := 'Podpowiedź: odnośnik został kliknięty w punkcie ' + IntToStr(X) + ' i ' + IntToStr(Y);
end;

Teraz mając informacje o współrzędnych X i Y (reprezentujących miejsce kliknięcia odnośnika), możesz w prosty sposób przedstawić je użytkownikowi.

Zachowanie komponentu

Podczas umieszczania komponentu na formularzu także zostaje wywoływany konstruktor Create. Z tego też powodu istnieje możliwość kontroli zachowania tego komponentu dzięki właściwości ComponentState (wspominałem o niej w poprzednim rozdziale). Możliwe wartości tego komponentu przedstawiłem w tabeli 15.1.

WartośćOpis
csDesigningAplikacja wykorzystująca komponent znajduje się na etapie projektowania
csLoadingZe strumienia do komponentu odczytywane są dane. Wartość przydzielana jest w momencie pierwszego tworzenia komponentu
csUpdatingKomponent jest aktualizowany
csWritingZapisywanie danych z komponentu do strumienia
csReadingOdczytywanie danych ze strumienia
csDestroyingZa chwilę komponent zostanie zniszczony

Komponenty graficzne

Zajmijmy się przez moment innymi typami komponentów, które nie są oparte na żadnej sprecyzowanej klasie typu TLabel czy TMemo. Stworzymy komponent praktycznie od początku. Za klasę bazową posłuży nam TCustomControl. Wybrałem ją dlatego, gdyż posiada zdarzenie OnPaint i klasę Canvas. Zaprojektujemy bowiem komponent graficzny, który ? po wprowadzeniu odpowiednich danych ? będzie na swojej powierzchni malował napis, przesuwający się i odbijający od ścianek komponentu. Działanie tego komponentu zostało przedstawione na rysunku 15.8.

15.8.jpg
Rysunek 15.8. Przykład działania komponentu

Tekst będzie trójwymiarowy, a ramka, którą widzisz, to krawędzie komponentu. Najtrudniejsze będzie zaprogramowanie procedury odbijania od ścianek i zapewnienie samego efektu losowości w odbijaniu tekstu. Całość działa mniej więcej tak: na samym początku losowane są dwie wartości, które reprezentować będą przesunięcie animacji z pozycji X i Y:

 iX := Random(4)+1;
 iY := Random(4)+1;

Do tej wartości dodawana jest jeszcze cyfra 1 (w celu zabezpieczenia się przed wylosowaniem zera). W komponencie tym wykorzystamy obiekt (komponent) TTimer, umożliwiający wykonywanie co jakiś czas określonych instrukcji. Stworzymy go w naszym komponencie w sposób dynamiczny. Obiekt TTimer będzie zatem wykonywał osobne zadanie ? dodawał do współrzędnych pozycji tekstu X i Y wylosowane wartości iX oraz iY. Po każdej takiej operacji trzeba będzie sprawdzać, czy przypadkiem napis nie dotknął ściany komponentu ? wtedy ponownie dokonujemy operacji losowania i napis zostaje przesunięty w innym kierunku.

Ogólny zarys klasy komponentu

Ten komponent będzie nieco trudniejszy od poprzedniego, który projektowaliśmy. Kod klasy przedstawia się następująco:

type
  TFly = class(TCustomControl)
  private
    FCaption : String;
    FActive : Boolean;
    FX, FY : Integer;
    Timer : TTimer;
    FInterval : Integer;
    F3D : TColor;
    FOnStart, FOnStop : TNotifyEvent;
    procedure SetActive(Value : Boolean);
    procedure SetInterval(Value : Integer);
    procedure SetCaption(ACaption : String);
    procedure SetFont(AFont : TFont);
  protected
    procedure OnTimer(Sender: TObject); // procedura obsługi Timera
  public
    constructor Create(AOwner : TComponent); override; // konstruktor
    destructor Destroy; override; // destruktor
    procedure Paint; override;  // procedura OnPaint
  published
    property Interval : Integer read FInterval write SetInterval;  // przerwa pomiędzy skokami
    property Active : Boolean read FActive write SetActive; // animacja aktywna czy nie?
    property Caption : String read FCaption write SetCaption;  // wyświetlany tekst
    property T3D : TColor read F3D write F3D; // drugi kolor, dający efekt 3D
  { standardowe zdarzenia }
    property Font write SetFont;
    property OnClick;
    property OnDblClick;
    property OnMouseDown;
    property OnMouseUp;
    property OnMouseMove;
  { dwa zdarzenia, które będą występować podczas zatrzymania lub uruchomienia animacji }
    property OnStart : TNotifyEvent read FOnStart write FOnStart;
    property OnStop : TNotifyEvent read FOnStop write FOnStop;
  end;

Chyba nie muszę objaśniać, jak stworzyć szablon dla nowego komponentu ? prezentowaliśmy to już ostatnio.
Omówmy jednak pobieżnie budowę samej klasy. W sekcji private znajdują się cztery procedury. Służą one do ustawienia pewnych wartości. Spójrz na sekcję published. Kilka metod odczytuje wartości ze zmiennej, a po słowie kluczowym write umieszczona jest nazwa procedury. Dzięki temu oprócz przypisania wartości do metody można w kodzie procedury zawrzeć jakieś dodatkowe czynności, czyli ? inaczej mówiąc ? określić, co program ma wykonać po przypisaniu danych do właściwości. Kluczową właściwością jest Active. Definiuje ona bowiem, czy animacja ma być uruchomiona. Jeżeli nie, napis nie będzie się poruszał ? zostanie jedynie wywołana metoda Paint, ale TTimer będzie wyłączony (właściwość Timer.Enabled). Jeżeli Active = TRUE, TTimer zostanie włączony.

Zwróć jeszcze uwagę na jedną rzecz, a mianowicie zdarzenia takie, jak OnMoueDown, OnMoueUp itd. Wpisanie ich nazw w zupełności wystarczy, aby te zdarzenia pojawiły się w Inspektorze Obiektów.

Kod źródłowy komponentu

Zacznijmy może od konstruktora. Jego zadaniem jest przypisanie paru metodom odpowiednich wartości:

constructor TFly.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  { utwórz obiekt TTimer }
  Timer := TTimer.Create(Self);
  Timer.Enabled := False;
  Interval := 500;  // przypisz właściwość Interval
  Timer.Interval := Interval;
  Timer.OnTimer := OnTimer;  // obsługa zdarzenia

  Height := 350;
  Width := 300;

  { ustawienia domyślne dla czcionki }
  Font.Style := [fsBold];
  Font.Size := 14;
  Font.Color := clBlack;
  F3D := clWhite;

  Randomize;
  { losuj początkową pozycję położenia tekstu }
  FX := Random(Width);
  FY := Random(Height);
  { wylosuj ilość pikseli o które animacja będzie się posuwać }
  iX := Random(4)+1;
  iY := Random(4)+1;

  FCaption := '4programmers.net';

  DoubleBuffered := True;
end;

Na samym jednak początku tworzony jest komponent TTimer i ustawiane są jego właściwości oraz procedura obsługi zdarzenia OnTimer. Następnie ustawiona zostaje domyślna czcionka, jaka będzie używana zaraz po umieszczeniu komponentu na formularzu. Zwróć uwagę na ostatni wiersz tego konstruktora. Zmieniam wartość zmiennej DoubleBuffered na True. Zmienna ta określa, czy podczas rysowania używane będzie tzw. podwójne buforowanie. Podczas gdy jest ona ustawiona na False, co jest wartością domyślną, rysowanie odbywa się bezpośrednio na komponencie, co może spowodować bardzo nielubiane przez programistów migotanie tekstu. Natomiast gdy zmienna ma wartość True, wszelkie zmiany dotyczące rysowania odbywają się w pamięci komputera, a dopiero później są przedstawiane na formularzu. Wiąże się to ze zwiększeniem zapotrzebowania na pamięć.

W konstruktorze obiektu nie zapomnij o zwolnieniu obiektu typu TTimer:

destructor TFly.Destroy;
begin
  Timer.Free; // zwolnij obiekt
  inherited;
end;

Podstawę dla komponentu już mamy ? teraz zajmijmy się sprawą rysowania. Już podczas umieszczania komponentu na formularzu będzie można zmienić właściwość Active na True i obserwować proces animacji. Jeżeli animacja będzie zatrzymana, tekst nie będzie się poruszał. Trzeba zatroszczyć się o to, aby użytkownik np. po zmodyfikowaniu czcionki podczas projektowania programu od razu zobaczył efekty? Trzeba zatem umożliwić podgląd zmian. Po każdorazowej zmianie ? czy to czcionki, czy czegoś innego ? tekst zostanie odświeżony, czyli ponownie zostanie wywołana metoda Paint:

procedure TFly.Paint;
begin
  Canvas.Font := Font; // ustaw czcionkę
  Canvas.Brush.Style := bsClear; // tekst przezroczysty

  Canvas.Font.Color := F3D;  // drugi kolor, dający efekt 3D
  Canvas.TextOut(Fx, Fy, FCaption); // narysuj najpierw drugim kolorem
  Canvas.Font.Color := Font.Color;  // ustaw teraz prawidłowy
  Canvas.TextOut(FX+1, FY+1, FCaption); // z minimalnym przesunięciem narysuj drugą warstwę
end;

Na samym początku tej procedury (w pierwszym wierszu) do klasy Canvas zostaje przypisana taka czcionka, jaka ustawiona jest w Inspektorze Obiektów. Później według ustawień Canvas narysowany zostanie tekst 3D. Nie jest to nic trudnego. Po prostu rysujemy ten sam tekst z minimalnym przesunięciem (np. 1 punktu) i ze zmienionym kolorem czcionki.

Pozostało nam jeszcze wprowadzanie animacji w ruch. Zrealizujemy to za pomocą komponentu TTimer i jego zdarzenia OnTimer:

procedure TFLy.OnTimer(Sender: TObject);
begin
{ do wartości zmiennych dodaj wylosowane pozycje iX oraz iY }
  FX := FX + iX;
  FY := FY + iY;
  Repaint; // przerysuj
  
  Canvas.Font.Color := F3D;
  Canvas.TextOut(Fx, Fy, FCaption);
  Canvas.Font.Color := Font.Color;
  Canvas.TextOut(FX+1, FY+1, FCaption);

{
   tutaj następuje sprawdzenie, czy animacja nie wychodzi poza brzegi komponentu.
   Jeżeli tak się stanie, trzeba dla zmiennych iX oraz iY wylosować nowe wartości,
   o które będzie się przesuwać animacja.
}
  Randomize;
  if FX < ?1 then iX := Random(4);
  if FY < ?1 then iY := Random(4);
  if FX >= (Width ? Canvas.TextWidth(FCaption)) then iX := ?Random(4);
  if FY >= (Height ? Canvas.TextHeight(FCaption)) then iY := ?Random(4);
end;

Za każdym wystąpieniem tego zdarzenia do obecnych wartości FX i FY zostają dodane wartości iX i iY, czyli wylosowane na początku wartości oznaczające, o ile punktów tekst będzie przewijany w pionie i poziomie. Animacja ta polega po prostu na każdorazowym przerysowaniu tekstu, tyle że w zmienionej pozycji i z uprzednim odświeżeniem. W ostatnich instrukcjach if tej metody sprawdzane jest, czy nie wystąpiła kolizja, tj. czy tekst nie dotknął krawędzi komponentu. Wtedy bowiem trzeba wylosować nowe wartości FX i FY, czyli nowe kierunki, w których przesuwany będzie tekst.

Pełny kod komponentu przedstawiony jest w listingu 15.3.

Listing 15.3. Kod źródłowy komponentu

{
  Copyright (c) 2002 by Adam Boduch
}

unit Fly;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  ExtCtrls;

type
  TFly = class(TCustomControl)
  private
    FCaption : String;
    FActive : Boolean;
    FX, FY : Integer;
    Timer : TTimer;
    FInterval : Integer;
    F3D : TColor;
    FOnStart, FOnStop : TNotifyEvent;
    procedure SetActive(Value : Boolean);
    procedure SetInterval(Value : Integer);
    procedure SetCaption(ACaption : String);
    procedure SetFont(AFont : TFont);
  protected
    procedure OnTimer(Sender: TObject); // procedura obsługi dla Timera
  public
    constructor Create(AOwner : TComponent); override; // konstruktor
    destructor Destroy; override; // destruktor
    procedure Paint; override;  // procedura OnPaint
  published
    property Interval : Integer read FInterval write SetInterval;  // przerwa pomiędzy skokami
    property Active : Boolean read FActive write SetActive; // animacja aktywna czy nie?
    property Caption : String read FCaption write SetCaption;  // wyświetlany tekst
    property T3D : TColor read F3D write F3D; // drugi kolor, dający efekt 3D
  { standardowe zdarzenia }
    property Font write SetFont;
    property OnClick;
    property OnDblClick;
    property OnMouseDown;
    property OnMouseUp;
    property OnMouseMove;
  { dwa zdarzenia, które będą występować podczas zatrzymania lub uruchomienia animacji }
    property OnStart : TNotifyEvent read FOnStart write FOnStart;
    property OnStop : TNotifyEvent read FOnStop write FOnStop;
  end;


procedure Register;

implementation

var iX, iY : Integer;

procedure TFly.SetInterval(Value : Integer);
begin
  FInterval := Value;
  Timer.Interval := FInterval;
end;

procedure TFly.SetCaption(ACaption : String);
begin
  FCaption := ACaption;
  Paint;
end;

procedure TFly.SetActive(Value : Boolean);
begin
  FActive := Value;
  Paint;
  Timer.Enabled := FActive;
  { tutaj następuje sprawdzenie, czy animacja została zatrzymana czy dopiero
  rozpoczęta; następnie wywoływane zostaje odpowiednie zdarzenie }
  if Value = True then if Assigned(FOnStart) then OnStart(Self);
  if Value = False then if Assigned(FOnStop) then OnStop(Self);
end;

procedure TFly.SetFont(AFont : TFont);
begin
  Font := AFont;
  Paint;
end;

procedure TFLy.OnTimer(Sender: TObject);
begin
{ do wartości zmiennych dodaj wylosowane pozycje iX oraz iY }
  FX := FX + iX;
  FY := FY + iY;
  Repaint; // przerysuj
  
  Canvas.Font.Color := F3D;
  Canvas.TextOut(Fx, Fy, FCaption);
  Canvas.Font.Color := Font.Color;
  Canvas.TextOut(FX+1, FY+1, FCaption);

{
   tutaj następuje sprawdzenie, czy animacja nie wychodzi poza brzegi komponentu.
   Jeżeli tak się stanie, trzeba dla zmiennych iX oraz iY wylosować nowe wartości,
   o które będzie się przesuwać animacja.
}
  Randomize;
  if FX < ?1 then iX := Random(4);
  if FY < ?1 then iY := Random(4);
  if FX >= (Width ? Canvas.TextWidth(FCaption)) then iX := ?Random(4);
  if FY >= (Height ? Canvas.TextHeight(FCaption)) then iY := ?Random(4);
end;


procedure TFly.Paint;
begin
  Canvas.Font := Font; // ustaw czcionkę
  Canvas.Brush.Style := bsClear; // tekst przezroczysty

  Canvas.Font.Color := F3D;  // drugi kolor, dający efekt 3D
  Canvas.TextOut(Fx, Fy, FCaption); // narysuj najpierw drugim kolorem
  Canvas.Font.Color := Font.Color;  // ustaw teraz prawidłowy kolor
  Canvas.TextOut(FX+1, FY+1, FCaption); // z minimalnym przesunięciem narysuj drugą warstwę
end;

constructor TFly.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  { utwórz obiekt TTimer }
  Timer := TTimer.Create(Self);
  Timer.Enabled := False;
  Interval := 500;  // przypisz właściwość Interval
  Timer.Interval := Interval;
  Timer.OnTimer := OnTimer;  // obsługa zdarzenia

  Height := 350;
  Width := 300;

  { domyślne ustawienia czcionki }
  Font.Style := [fsBold];
  Font.Size := 14;
  Font.Color := clBlack;
  F3D := clWhite;

  Randomize;
  { losuj początkową pozycję dla położenia tekstu }
  FX := Random(Width);
  FY := Random(Height);
  { wylosuj ilość pikseli, o które animacja będzie się przesuwać }
  iX := Random(4)+1;
  iY := Random(4)+1;

  FCaption := '4programmers.net';

  DoubleBuffered := True;
end;

destructor TFly.Destroy;
begin
  Timer.Free; // zwolnij obiekt
  inherited;
end;

procedure Register;
begin
  RegisterComponents('Samples', [TFly]);
end;


end.

Tak przedstawia się cały kod tego komponentu. Po zainstalowaniu możesz spokojnie z niego korzystać. Do tej książki dołączony jest także program demonstrujący korzystanie z tego komponentu.

Pakiety komponentów

Czasem może zajść potrzeba zainstalowania kilku (kilkunastu?) komponentów Twojego autorstwa. Co wtedy? Przecież nie będziesz instalował każdego komponentu z osobna. Dobrym rozwiązaniem jest w tym wypadku zastosowanie pakietów komponentu. Dlatego też dla przykładu napisałem dwa proste komponenty ? TGetUser, który podaje nazwę zalogowanego użytkownika systemu oraz TGetWindows, który to komponent podaje ścieżkę do katalogu, w którym zainstalowany jest system Windows. Oba kody źródłowe przedstawione są w listingach 15.4 i 15.5.

Listing 15.4. Kod źródłowy komponentu TGetUser

{
   Copyright (c) 2002 by Adam Boduch
}

unit GetUser;

interface

uses
  Windows, Messages, SysUtils, Classes, Controls;

type
  TGetUser = class(TComponent)
  public
    function GetUser : String; // funkcja zwraca nazwę zalogowanego użytkownika
  end;

procedure Register;

implementation

function TGetUser.GetUser : String;
var
  Buffer : array[0..128] of char;
  Size : DWORD;
begin
  Size := 128;
  GetUserName(Buffer, Size); // wywołaj procedurę GetUserName
  Result := Buffer;
end;

procedure Register;
begin
  RegisterComponents('Samples', [TGetUser]);
end;

end.

Listing 15.5. Kod źródłowy komponentu TGetWindows

{
  Copyright (c) 2002 by Adam Boduch
}

unit GetWindows;

interface

uses
  Windows, Messages, SysUtils, Classes;

type
  TGetWindows = class(TComponent)
  public
    function GetWDirectory : String;
  end;

procedure Register;

implementation

function TGetWindows.GetWDirectory : String;
var
  Buffer : array[0..255] of char;
begin
  GetWindowsDirectory(Buffer, SizeOf(Buffer)); // podaje ścieżkę do katalogu, w którym zainstalowany jest system
  Result := Buffer;
end;

procedure Register;
begin
  RegisterComponents('Samples', [TGetWindows]);
end;

end.

Komponenty te obsługuje się bardzo łatwo, lecz możesz stworzyć pakiet, który posłuży do jednoczesnego ich zainstalowania. W tym celu z menu File wybierz polecenie Other. Następnie kliknij dwukrotnie ikonę Package. Na ekranie pojawi się takie okienko, jak na rysunku 15.9.

15.9.jpg
Rysunek 15.9. Tworzenie nowego pakietu

Przycisk Add służy do dodawania nowych modułów do pakietu. Moduły te zostaną dodane do gałęzi Contains. Po naciśnięciu tego przycisku zostanie wyświetlone okno ? wystarczy wówczas podać nazwę modułu, w którym znajduje się komponent. Następnie możesz taki pakiet zapisać, wybierając z menu File polecenie Save. Teraz wystarczy, że po otwarciu takiego pakietu naciśniesz przycisk Install, a Delphi zainstaluje wszystkie komponenty zawarte w pakiecie.

Po naciśnięciu przycisku Compile moduły zawarte w tym pakiecie zostaną skompilowane do postaci plików DCU. Jak już mówiłem wcześniej, jeżeli komponent został skompilowany w Delphi 7, to do zainstalowania go na innym komputerze, gdzie także jest zainstalowane Delphi 7, nie potrzeba kodów źródłowych.

Podsumowanie

Nie da się ukryć, że projektowanie komponentów jest kolejnym stopniem wtajemniczenia w nauce Delphi. Ty tę sztukę właśnie opanowałeś ? uwierz mi, że ta nauka nie pójdzie na marne. Być może będziesz w przyszłości pisał komercyjne komponenty, które później będziesz sprzedawał? Tak, tak ? taki rodzaj zarobkowania jest często spotykany w sieci.

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

Niestety brak ilustracji.

Udało się :)

Wystarczyło dodać @ przed OmTimer...
Lazarus :)

Całkiem, całkiem :)