Rozdział 7. Programowanie obiektowe

Adam Boduch

W trakcie omawiania języka Delphi nie sposób nie wspomnieć o programowaniu obiektowym. Termin ten przewijał się w tej książce już wcześniej. Teraz zajmiemy się projektowaniem własnych klas oraz omówieniem zasad projektowania obiektowego.

     1 Klasy
          1.1 Składnia klasy
          1.2 Do czego służą klasy?
          1.3 Hermetyzacja
          1.4 Dziedziczenie
          1.5 Polimorfizm
          1.6 Instancja klasy
          1.7 Konstruktor
          1.8 Destruktor
          1.9 Pola
          1.10 Metody
          1.11 Tworzenie konstruktorów i destruktorów
          1.12 Destruktory w .NET
               1.12.1 Finalize
               1.12.2 Dispose
               1.12.3 Po co destruktor?
          1.13 Poziomy dostępu do klasy
               1.13.4 Sekcja private
               1.13.5 Sekcja protected
               1.13.6 Sekcja public
               1.13.7 Dodatkowe oznaczenia w .NET
          1.14 Dziedziczenie
               1.14.8 Klasa domyślna
          1.15 Przeciążanie metod
          1.16 Typy metod
               1.16.9 Metody wirtualne a metody dynamiczne
               1.16.10 Metody abstrakcyjne
          1.17 Przedefiniowanie metod
               1.17.11 Reintrodukcja metody
               1.17.12 Słowo kluczowe inherited
               1.17.13 Inherited w konstruktorach
          1.18 Typy zagnieżdżone
          1.19 Parametr Self
          1.20 Brak konstruktora
          1.21 Brak instancji klasy
          1.22 Class helpers
          1.23 Klasy zaplombowane
          1.24 Słowo kluczowe static
          1.25 Właściwości
               1.25.14 Wartości domyślne
     2 Parametr Sender procedury zdarzeniowej
          2.26 Przechwytywanie informacji o naciśniętym klawiszu
               2.26.15 Obsługa zdarzeń przez inne komponenty
          2.27 Obsługa parametru Sender
     3 Operatory is i as
     4 Metody w rekordach
     5 Interfejsy
     6 Przeładowanie operatorów
          6.28 Jakie operatory można przeładować?
          6.29 Deklaracja operatorów
          6.30 Binary i Unary
     7 Wyjątki
          7.31 Słowo kluczowe try..except
          7.32 Słowo kluczowe try..finally
               7.32.16 Zagnieżdżanie wyjątków
          7.33 Słowo kluczowe raise
     8 Klasa Exception
          8.34 Selektywna obsługa wyjątków
          8.35 Zdarzenie OnException
               8.35.17 Obsługa wyjątków
     9 Identyfikatory
     10 Boksowanie typów
     11 Przykład wykorzystania klas
          11.36 Zasady gry
          11.37 Specyfikacja klasy
               11.37.18 Ustawienia gracza
               11.37.19 Obsługa wyjątków
          11.38 Zarys klasy
          11.39 Sprawdzenie wygranej
          11.40 Interfejs aplikacji
               11.40.20 Ćwiczenie dodatkowe
          11.41 Tworzenie interfejsu graficznego
          11.42 Gra "Kółko i krzyżyk"
               11.42.21 Obsługa właściwości Tag
     12 Biblioteka VCL/VCL.NET
          12.43 Klasa TApplication
               12.43.22 Właściwości klasy TApplication
                    12.43.22.1 Active
                    12.43.22.2 ExeName
                    12.43.22.3 ShowMainForm
                    12.43.22.4 Title
                    12.43.22.5 Icon
               12.43.23 Metody klasy TApplication
                    12.43.23.6 Minimize
                    12.43.23.7 Terminate
                    12.43.23.8 MessageBox
                    12.43.23.9 ProcessMeessages
                    12.43.23.10 Restore
               12.43.24 Zdarzenia klasy TApplication
          12.44 Właściwości
               12.44.25 Align
               12.44.26 Anchors
               12.44.27 Constraints
               12.44.28 Cursor
               12.44.29 DragCursor, DragKind, DragMode
               12.44.30 Font
               12.44.31 HelpContex, HelpKeyword, HelpType
               12.44.32 Hint, ShowHint
               12.44.33 Visible
               12.44.34 Tag
          12.45 Zdarzenia
               12.45.35 OnClick
               12.45.36 OnContextPopup
               12.45.37 OnDblClick
               12.45.38 OnActivate, OnDeactivate
               12.45.39 OnClose, OnCloseQuery
               12.45.40 OnPaint
               12.45.41 OnResize
               12.45.42 OnShow, OnHide
               12.45.43 OnMouseDown, OnMouseMove, OnMouseUp, OnMouseWheel, OnMouseWheelDown, OnMouseWheelUp
               12.45.44 Zdarzenia związane z dokowaniem
                    12.45.44.11 OnDockDrop
                    12.45.44.12 OnDockOver
                    12.45.44.13 OnStartDock
                    12.45.44.14 OnStartDrag
                    12.45.44.15 OnEndDrag, OnEndDock
                    12.45.44.16 OnDragDrop
                    12.45.44.17 OnDragOver
                    12.45.44.18 Przykładowy program
     13 Programowanie w .NET
          13.46 Wspólny model programowania
          13.47 Klasa System.Object
     14 Test
     15 FAQ
          15.48 Podsumowanie

W tym rozdziale:
*zdefiniuję pojęcie klasy,
*opowiem, czym charakteryzuje się programowanie obiektowe,
*pokażę, jak wykorzystywać wyjątki w swoim programie,
*opiszę, czym jest przeciążanie operatorów.

Klasy

Klasy stanowią pewien element, zbiór procedur i funkcji. Jest to specyficzna część języka, obecna w większości języków programowania wysokiego poziomu. Obecnie idea programowania obiektowego jest bardzo popularna, a właśnie klasy są kluczowym elementem owej idei.

Cała biblioteka VCL.NET jest oparta na klasach wzajemnie czerpiących z siebie potrzebne funkcje i właściwości. Również platforma .NET dostarcza szereg klas (w tym klas związanych z Windows Forms).

Ważne jest zrozumienie, na czym polega programowanie obiektowe, gdyż Czytelnik na pewno zetknie się z tym pojęciem, jeśli tylko zechce poszerzać swoje umiejętności programistyczne w językach takich jak C# lub Java, gdzie klasy stanowią podstawę projektowania aplikacji.

Składnia klasy

W języku Delphi klasę deklaruje się z użyciem słowa kluczowego class:

type
  NazwaKlasy = class (KlasaBazowa)
     Metody
  end;

Klasę należy deklarować jako nowy typ danych. Czyli:
*klasa musi zostać zadeklarowana z użyciem słowa kluczowego class,
*klasa musi być deklarowana jako typ (type),
*klasa musi mieć nazwę,
*klasę należy zakończyć słowem kluczowym — end;.

Zwróćmy uwagę, że po słowie kluczowym class nie ma średnika!

Najprostsza deklaracja klasy może wyglądać następująco:

type
  MojaKlasa = class
  end;

Od tego momentu będzie można zadeklarować nową zmienną typu MojaKlasa:

var
  Moja : MojaKlasa;

Skoro klasa jest deklarowana jako typ, w każdym momencie można utworzyć zmienną wskazującą na ów typ. Taka konstrukcja jak powyżej jest nazywana tworzeniem instancji klasy lub egzemplarza klasy. Możesz się spotkać również z pojęciem tworzenia obiektu klasy.

Zalecane jest specjalne nazewnictwo w przypadku klas, z uwzględnieniem litery T jako pierwszej litery nazwy klasy — np. TObject, TRegistry, TMojaKlasa itp. Taka konwencja przyjęła się dzięki bibliotece VCL, w której nazwa każdej klasy jest poprzedzana literą T. Nie zamierzam odbiegać od tej reguły — swoje klasy także będę nazywał, zaczynając od litery T.

Do czego służą klasy?

Początkującemu programiście zapewne trudno będzie dostrzec zalety korzystania z klas. Zawsze można zrealizować ten sam cel, posługując się zmiennymi globalnymi oraz procedurami i funkcjami. To fakt, jednak stosowanie klas daje spore możliwości, z których warto wymienić: umieszczanie w niej odpowiednich metod i ich ukrywanie, albo też tworzenie kilku instancji danej klasy. Ponadto umożliwia uporządkowanie kodu i podzielenie go na kilka oddzielnych części, z których każda wykonuje inną czynność podczas działania programu.

Przykładowo, aby złożyć komputer, nie muszę wiedzieć, jak dokładnie działa procesor i z jakich elementów jest zbudowany. Wystarczy że wiem, że jest to centralna jednostka komputera i że bez procesora nie uruchomię całości. Muszę także wiedzieć, gdzie włożyć ten procesor i jak go przymocować.

Kierowca samochodu nie musi wiedzieć, co auto ma pod maską, jakie są parametry jego silnika, jak działa skrzynia biegów i co powoduje, że całość się porusza. Wystarczy że wie, iż do uruchomienia samochodu potrzebne są kluczyki — musi również umieć posługiwać się kierownicą, dźwignią zmiany biegów i pedałami.

Jeżeli wraz ze swoimi wspólnikami projektujecie jakąś większą aplikację, każdy może zająć się przydzielonym zadaniem — przykładowo, ktoś zajmuje się utworzeniem klasy służącej do wyszukiwania plików na dysku, jeszcze ktoś tworzeniem innej klasy, a inna osoba jedynie wszystko koordynuje i łączy w całość. Nie musi ona wiedzieć, w jaki sposób działa klasa wyszukująca pliki, ale musi wiedzieć, jak ją połączyć z resztą programu, tak aby wszystko działało zgodnie z oczekiwaniami. Tego z kolei można się dowiedzieć z instrukcji (czyli z dokumentacji dostarczonej przez autora klasy).

Hermetyzacja

Pojęcie hermetyzacji jest związane z ukrywaniem pewnych danych. Klasy udostępniają na zewnątrz pewien interfejs opisujący działanie takiej klasy i tylko z tego interfejsu może korzystać użytkownik. Bowiem klasy mogą zawierać dziesiątki, a nawet setki metod (procedur lub funkcji), które wykonują różne czynności. My jako projektanci klasy powinniśmy zapewnić dostęp jedynie do niektórych metod, tak aby potencjalny użytkownik nie mógł wykorzystywać wszystkich, gdyż może to spowodować nieprzewidywalne działanie programu, zawieszanie itp.

Wewnątrz silnika samochodu też dochodzi do pewnych procesów, ale kierowca nie musi o nich wiedzieć. Informacji tych nie potrzebuje także inny element silnika, który się z nim łączy — komunikowanie się pomiędzy elementami przebiega ustalonym strumieniem i to wystarczy.

Delphi pozwala na ukrywanie kodu w klasie, w tym celu stosuje się pewne klauzule, o których powiemy sobie później.
Metody są procedurami i funkcjami znajdującymi się w klasie i współpracującymi z sobą w celu wykonania konkretnych czynności.

Metody są procedurami i funkcjami znajdującymi się w klasie i współpracującymi z sobą w celu wykonania konkretnych czynności.

Dziedziczenie

Cała wizualna biblioteka VCL.NET jest oparta na dziedziczeniu, które można określić jako podstawowy fundament budowania klas.

Powróćmy do przykładu z silnikiem. Projektanci, chcąc ulepszyć dany silnik, mogą nie zechcieć zaczynać od zera. Byłaby to zwyczajna strata czasu. Nie lepiej po prostu unowocześnić silnik już istniejący?

Przykład z silnikiem można zastosować do klas. Aby zbudować nową, bardziej funkcjonalną klasę, można przejąć możliwości starej. Taki proces nazywamy w programowaniu dziedziczeniem. Na przykład w VCL.NET podstawową klasą jest klasa o nazwie TObject. Zaprogramowano w niej podstawowe mechanizmy oraz funkcje. Klasą, która dziedziczy po niej, jest TRegistry. Inaczej mówiąc, przejmuje ona wszystkie dotychczasowe możliwości TObject oraz dodaje własne — w tym przypadku obsługę rejestru Windows.

W takim przypadku klasę TObject nazywamy klasą potomną (lub po prostu potomkiem), a TRegistry klasą dziedziczną.

Jeżeli Czytelnik zainteresował się tematyką obsługi rejestru w systemie Windows, zalecam zapoznanie się z opisem klasy TRegistry w pomocy elektronicznej Delphi. Odpowiedni rozdział poświęcony rejestrom znajduje się również w książce Delphi 7. Kompendium programisty, wydanej nakładem wydawnictwa Helion w 2003 roku oraz na stronie internetowej http://programowanie.org/delphi.

Dziedziczenie ma też zastosowanie w .NET Framework Class Library (FCL), gdzie główną klasą jest klasa System.Object, a wszystkie pozostałe dziedziczą po niej.

Polimorfizm

Pojęcie polimorfizmu jest związane z dziedziczeniem. Jest to po prostu możliwość tworzenia wielu metod o tej samej nazwie. Przykładowo, jeżeli klasa A zawiera metodę XYZ i jeżeli klasa B dziedziczy po klasie A, to tym samym posiada również jej metodę XYZ. Teraz istnieje możliwość „przykrycia” owej metody XYZ i zaimplementowanie takiej samej, tyle że w klasie B.

Instancja klasy

Aby móc wykorzystywać metody znajdujące się w klasie, należy utworzyć tzw. instancję owej klasy. W tym momencie zostaje zarezerwowana pamięć potrzebna do wykonania metod znajdujących się w tej klasie. Istotną sprawą jest to, że może istnieć wiele instancji danej klasy. Jest to przewaga w stosunku do idei programowania strukturalnego. Każda instancja rezerwuje osobny blok pamięci. Ewentualne zmienne (pola) znajdujące się w obrębie klasy korzystają z osobnych przestrzeni adresowych i mogą mieć różne wartości.

Konstruktor

Aby utworzyć klasę i móc ją stosować, należy skorzystać z tzw. konstruktora. Konstruktor jest specjalną metodą dziedziczoną po klasie TObject (w przypadku VCL.NET), za pomocą której jest możliwe zainicjalizowanie klasy.

Oto przykład inicjalizacji klasy:

var
  Klasa : TMojaKlasa; // wskazanie na nowy typ – klasę
begin
  Klasa := TMojaKlasa.Create; // wywołanie konstruktora
end;

Jak widać, konstrukcja jest w tym przypadku dość specyficzna:

Zmienna := NazwaKlasy.Create;

Taka instrukcja spowoduje utworzenie klasy — od tej pory można z niej dowolnie korzystać.

Destruktor

Destruktor jest specjalną metodą utworzoną w celu destrukcji klasy, czyli zwolnienia pamięci. Tak więc konstruktor powinien być wywoływany na samym początku — przed skorzystaniem z klasy, a destruktor — na samym końcu, kiedy dana klasa już jest niepotrzebna. Destruktor również jest metodą dziedziczoną po klasie TObject — oto przykład jej wywołania:

var
  Klasa : TMojaKlasa; // wskazanie na nowy typ – klasę
begin
  Klasa := TMojaKlasa.Create; // wywołanie konstruktora
  Klasa.Free; // zwolnienie klasy
end;

Destrukcja klasy odbywa się poprzez wywołanie metody Free.

Pola

Klasa oprócz metod może przechowywać pewne informacje, do których aplikacja spoza klasy może uzyskać dostęp. Pola (czyli dane znajdujące się w klasie) mogą być również wymieniane pomiędzy klasami lub istnieć jedynie na potrzeby naszej klasy.
Pola po prostu są zmiennymi lub stałymi, deklarowanymi na użytek klasy lub udostępnionymi na zewnątrz klasy do użytku programisty. Uzyskiwanie do nich dostępu jest możliwe za pośrednictwem operatora odwołania, np.:

MojaKlasa.Zmienna := 'Ala';

Jednakże wcześniej należy zadeklarować odpowiednie pole w klasie — wygląda to tak:

type
  TMojaKlasa = class
    Zmienna : String;
  end;

Określanie, czy dane pole ma być zmienną czy stałą, jest niekonieczne, aczkolwiek możliwe, np.:

type
  TMojaKlasa = class
    var Zmienna : String;
    const Stała = True;
  end;

W takim przypadku oczywiście pole @@Stała@@ będzie jedynie do odczytu, gdyż jest to stała.

Metody

Pojęcie metody oznacza procedury i funkcje zawarte w klasie. Ich definicja jest stosunkowo prosta — wygląda podobnie jak w przypadku pól:

type
  TMojaKlasa = class
    procedure Main;
  end;

Oprócz deklaracji procedury należy również zdefiniować jej ciało w sekcji Implementation:

procedure TMojaKlasa.Main;
begin

end;

Warto zwrócić uwagę na specyficzny zapis takiej procedury. W nagłówku należy określić, że dana metoda należy do klasy (TMojaKlasa.Main) — inaczej Delphi wygeneruje błąd: [Error] Engine.pas(20): Unsatisfied forward or external declaration: 'TTMojaKlasa.xxx'.

W Delphi działa pewien przyjazny skrót klawiaturowy, Ctrl+Shift+C, służący do generowania ciała procedury. Wystarczy naprowadzić wskaźnik myszy na definicję metody w klasie, a następnie nacisnąć kombinację klawiszy Ctrl+Shift+C — Delphi samodzielnie wygeneruje ciało metody w sekcji Implementation.

Teraz utworzenie klasy (wywołanie konstruktora) oraz wywołanie jakiejś metody i następnie zwolnienie tej klasy wygląda tak:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Main : TMain;
begin
  Main := TMain.Create;
  Main.Main;
  Main.Free;
end;

Metody w klasie muszą być deklarowane na samym końcu. Najpierw więc należy zadeklarować zmienne, a później metody. Inaczej Delphi zgłosi błąd: [Error] Engine.pas(38): Field definition not allowed after methods or properties.

Tworzenie konstruktorów i destruktorów

Konstruktory i destruktory dziedziczone po klasie bazowej nie są jedynym rozwiązaniem. Możliwe jest deklarowanie swojego konstruktora i destruktora według własnych wymagań. Konstruktor deklaruje się tak samo jak zwykłą metodę, tyle że z użyciem słowa kluczowego constructor. Analogicznie, destruktor deklaruje się z użyciem słowa kluczowego destructor. Oto nasza przykładowa klasa:

  { kod właściwej klasy }
  TMain = class
    FValue : String; // pole
    procedure Main; // metoda
    constructor MainCreate(Value : String); // konstruktor
  end;

Konstruktor jest elementem klasy wywoływanym przy jej tworzeniu. Często programiści stosują go do przekazania do klasy jakichś ważnych danych. W tym przypadku konstruktor MainCreate musi zostać wywołany z parametrem typu String.

Podczas tworzenia klasy należy oczywiście przekazać do konstruktora jakiś ciąg znakowy:

Main := TMain.MainCreate('Cześć! Jestem tekstem przekazywanym do konstruktora!');

Naturalnie, zawsze można skorzystać z domyślnego konstruktora o nazwie Create. Listing 7.1 przedstawia cały kod modułu, a poniżej omówiłem szczegóły klasy.

Listing 7.1. Kod modułu

unit WinForm2;

interface

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data;

type
  TWinForm2 = class(System.Windows.Forms.Form)
  {$REGION 'Designer Managed Code'}
  strict private
    /// <summary>
    /// Required designer variable.
    /// </summary>
    Components: System.ComponentModel.Container;
    Button1: System.Windows.Forms.Button;
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    procedure InitializeComponent;
    procedure Button1_Click(sender: System.Object; e: System.EventArgs);
  {$ENDREGION}
  strict protected
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    procedure Dispose(Disposing: Boolean); override;
  private
    { Private Declarations }
  public
    constructor Create;
  end;

  { kod właściwej klasy }
  TMain = class
    FValue : String; // pole
    procedure Main; // metoda
    constructor MainCreate(Value : String); // konstruktor
  end;

  [assembly: RuntimeRequiredAttribute(TypeOf(TWinForm2))]

implementation

{$REGION 'Windows Form Designer generated code'}
/// <summary>
/// Required method for Designer support -- do not modify
/// the contents of this method with the code editor.
/// </summary>
procedure TWinForm2.InitializeComponent;
begin
  Self.Button1 := System.Windows.Forms.Button.Create;
  Self.SuspendLayout;
  // 
  // Button1
  // 
  Self.Button1.Location := System.Drawing.Point.Create(128, 128);
  Self.Button1.Name := 'Button1';
  Self.Button1.TabIndex := 0;
  Self.Button1.Text := 'Button1';
  Include(Self.Button1.Click, Self.Button1_Click);
  // 
  // TWinForm2
  // 
  Self.AutoScaleBaseSize := System.Drawing.Size.Create(5, 13);
  Self.ClientSize := System.Drawing.Size.Create(292, 273);
  Self.Controls.Add(Self.Button1);
  Self.Name := 'TWinForm2';
  Self.Text := 'WinForm2';
  Self.ResumeLayout(False);
end;
{$ENDREGION}

procedure TWinForm2.Dispose(Disposing: Boolean);
begin
  if Disposing then
  begin
    if Components <> nil then
      Components.Dispose();
  end;
  inherited Dispose(Disposing);
end;

constructor TWinForm2.Create;
begin
  inherited Create;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent;
  //
  // TODO: Add any constructor code after InitializeComponent call
  //
end;

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Main : TMain;
begin
  Main := TMain.MainCreate('Cześć! Jestem tekstem przekazywanym do konstruktora!');
  Main.Main; // wywołanie metody
  Main.Free;
end;

constructor TMain.MainCreate(Value: String);
begin
  inherited Create;
  FValue := Value; // przypisujemy wartość przekazana do konstruktora
end;

procedure TMain.Main;
begin
{ wyświetlenie wartości z klasy }
  MessageBox.Show(FValue);
end;

end.

W konstruktorze do zmiennej @@FValue@@ (pola w klasie) zostaje przypisana wartość przekazana jako parametr do konstruktora. Następnie metoda Main odczytuje ten parametr i wyświetla go w oknie informacyjnym.

Nie było tutaj konieczne deklarowanie własnego destruktora — można użyć standardowego. Czasami jednak, programista potrzebuje destruktora, deklaruje się go tak:

destructor Destroy; override;

W przypadku Delphi dla .NET destruktor musi być opatrzony dyrektywą override (o tym powiem kilka akapitów dalej). W Delphi dla Win32 nie ma takiego wymogu.

Należy pamiętać, że w Delphi dla .NET destruktor musi mieć taką konstrukcję, jaka została przedstawiona powyżej (żadna inna nie zostanie przyjęta przez Delphi, gdyż zostanie wyświetlony taki komunikat o błędzie: [Error] WinForm2.pas(40): Unsupported language feature: 'destructor').

W metodzie Main skorzystałem z klasy MessageBox oferowanej przez .NET. Owa klasa umożliwia wyświetlanie okna z podanym tekstem, przekazanym jako parametr do procedury Show. Klasa MessageBox działa tak samo jak funkcja MessageBox ze standardowego zestawu bibliotek Windows API.

Destruktory w .NET

.NET zmienia nieco koncepcję postępowania z destruktorami. W środowisku Win32 wywoływanie destruktora w celu zwolnienia klasy oraz pamięci było niezbędnym postępowaniem. Jeśli programista nie zapewnił tego po zamknięciu programu, w pamięci nadal pozostawały dane programu. W .NET funkcjonuje mechanizm zwany garbage collection, który po zamknięciu programu może stwierdzić, czy dany obiekt będzie jeszcze przydatny. Jeżeli nie — mechanizm ów zwalnia go automatycznie.

Mimo to zaleca się wywoływanie metody Free klasy — dajemy tym samym do zrozumienia innym programistom czytającym kod, że w tym miejscu kończy się działanie klasy i jej dalsze używanie nie jest już potrzebne.

Teraz Czytelnikowi należą się pewne wyjaśnienia i doprecyzowanie niektórych faktów, gdyż w Delphi 2005 mechanizm destrukcji obiektów jest nieco bardziej rozbudowany.

Dla Czytelnika ważne jest to, że każda klasa posiada metodę zwaną Free, dzięki której można zwolnić tę klasę w jakimkolwiek momencie. Nie ma za to metody Destroy, która była obecna w poprzednich wersjach Delphi i odpowiadała metodzie Free, czyli również zwalniała klasę. Możliwe jest jednakże zadeklarowanie na potrzeby własnej klasy swojej metody Destroy:

  TFiat = class
    procedure Main;
    constructor Create;
    destructor Destroy; override;
  end;

Jeżeli metoda Destroy ma zwalniać klasę, należy ją zadeklarować jako destruktor:

destructor TFiat.Destroy;
begin
  MessageBox.Show('Zwalnianie klasy');
  inherited;
end;

Trzeba pamiętać o słowie inherited na końcu kodu destruktora!

W Delphi dla .NET istnieją pewne ograniczenia co do deklaracji destruktora — od teraz musi on posiadać z góry określoną budowę:
*destruktor musi nosić nazwę Destroy,
*destruktor musi posiadać klauzulę override,
*destruktor nie może posiadać parametrów.

Po spełnieniu tych warunków można korzystać z destruktora (metoda Destroy) tak samo jak w poprzednich wersjach Delphi:

var
  Fiat : TFiat;

begin
  Fiat := TFiat.Create;
  Fiat.Destroy;
end.

Jestem pewien, że Czytelnik nie będzie musiał zbyt często stosować takiego zapisu, gdyż programiści już wcześniej rzadko stosowali zapis Destroy — przeważnie zwalnianie klasy odbywało się poprzez metodę Free.

Taki sposób używania destruktora jest wskazany, jeżeli np. konstruktor klasy zawierał kod łączący się z bazą danych — wówczas kod destruktora winien takie połączenie zwalniać.

Finalize

Każda klasa w Delphi może posiadać metodę Finalize, która odgrywa ważną rolę w procesie zwalniania obiektów. Mechanizm garbage collector [#]_ wywołuje tę metodę automatycznie tuż przed zwolnieniem obiektu. Z metody Finalize nie można jednak skorzystać jawnie (związane jest to z dziedziczeniem, a konkretniej z tym, że owa metoda znajduje się w sekcji protected; będzie o tym mowa później) — trzeba ją zadeklarować we własnej klasie:

  TFiat = class
    procedure Main;
    procedure Finalize; override;
    constructor Create;
  end;

Również wymogiem jest inicjalizowanie metody Finalize z użyciem słowa kluczowego override (należy o tym pamiętać!).
Poniżej zaprezentowałem źródłowy fragment przykładowego modułu:

{ obsługa zdarzenia Click }
procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Fiat : TFiat;

begin
  Fiat := TFiat.Create;
  Fiat.Main;
end;

{ TFiat }

constructor TFiat.Create;
begin
  inherited Create;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Finalize;
begin
  MessageBox.Show('Destrukcja klasy');
  inherited;
end;

procedure TFiat.Main;
begin
  { kod }
end;

end.

W klasie TFiat znajduje się konstruktor oraz metoda Finalize. Na formularzu jest jeden przycisk. Wygenerowałem jego zdarzenie Click i wpisałem kod mający na celu inicjalizację klasy, ale nie jej zwolnienie! Należy zwrócić uwagę, że nigdzie nie ma kodu powodującego zwolnienie obiektu.

Po uruchomieniu programu i kliknięciu przycisku na ekranie zostaje wyświetlony komunikat: Inicjalizacja klasy. Teraz należy zamknąć program. Łatwo zauważyć, że w momencie zamykania programu zostanie wyświetlony komunikat: Destrukcja klasy. Tutaj właśnie zareagował mechanizm garbage collection, który niezależnie od programisty określił moment, gdy obiekt TFiat nie jest już potrzebny i wywołał metodę Finalize zwalniającą pamięć (a przy okazji wyświetlającą nasz komunikat).

Dispose

Zwalnianiem obiektu, który nie jest już potrzebny, zajmuje się automatyczny odśmiecacz. Tuż przed przejęciem obiektu przez odśmiecacz jest wywoływana metoda Finalize (pod warunkiem, że została zadeklarowana w obiekcie). Jeżeli dana klasa otwiera jakiś plik czy nawiązuje połączenie z bazą danych, może zajść potrzeba wywołania metody, która zwolni klasę oraz połączenie z bazą danych przed działaniem odśmiecacza.

W .NET służy do tego metoda Dispose, która jest zawarta w interfejsie IDisposable (o interfejsach powiem w dalszej części rozdziału). W Delphi dla .NET jest to równoznaczne z zadeklarowaniem destruktora:

destructor Destroy; override; 

Wiele osób programujących w Delphi przyzwyczaiło się do starego zapisu destruktora, zatem kompilator dyskretnie zamieni zapis związany z destruktorem na następujący:

TFiat = class(TObject, IDisposable)
  public
    constructor Create;
    procedure Dispose;
  end;

constructor TFiat.Create;
begin
  inherited;
  MessageBox.Show('Inicjalizacja klasy');
end;

procedure TFiat.Dispose;
begin
  MessageBox.Show('Destrukcja klasy!')
end;

Aby skorzystać z metody Dispose, należy określić, iż dana klasa TFiat dziedziczy również po interfejsie IDisposable. Można wówczas zadeklarować metodę Dispose, która działa identycznie jak destruktor:

var
  Fiat : TFiat;
begin
  Fiat := TFiat.Create;
  Fiat.Free;
end;

Wywołanie metody Free jest równoznaczne wywołaniu metody Dispose. Kod ten mógłby wyglądać także następująco:

  TFiat = class(TObject)
  public
    constructor Create;
    destructor Destroy; override;
  end;

{ ... metody klasy }
destructor TFiat.Destroy;
begin
  MessageBox.Show('Destrukcja klasy!')
  inherited;
end;

Działanie programu będzie identyczne w obu przypadkach.

Po co destruktor?

Po tym co napisałem, Czytelnik może zadać proste pytanie: po co w .NET stosować destruktory, skoro samo środowisko zajmie się zwalnianiem obiektów? Mechanizm garbage collection zapewnia zmniejszenie liczby błędów związanych z przeciekami pamięci (czyli pominięciem zwalniania pamięci w trakcie programowania), lecz nie oznacza to, że można całkowicie zrezygnować z destruktorów. Często destruktory są używane nie tylko do zwalniania pamięci, ale także do innych zadań, takich jak zamykanie pliku, który został otwarty na potrzeby danej klasy itp. Często należy odpowiednio zareagować na destrukcję obiektu i wykonać stosowne czynności — wówczas konieczne stanie się jawne wywołanie destruktora.

Poziomy dostępu do klasy

Wcześniej wspominałem o hermetyzacji, czyli ukrywaniu szczegółów klasy przed użytkownikiem, jednak jak dotąd nie pokazałem, w jaki sposób można skorzystalić z tej możliwości. Język Delphi oferuje kilka klauzul umożliwiających oznaczenie poziomu dostępu do danej metody lub zwykłego pola. Dodatkowo za sprawą przystosowania do .NET w Delphi zostały dodane dodatkowe klauzule.

Delphi udostępnia trzy główne poziomy dostępu do klasy — private (prywatne), protected (chronione), public (publiczne). W zależności od sekcji, w której metody zostaną umieszczone, będą one inaczej interpretowane przez kompilator. Dodatkowo jest możliwe jeszcze umieszczenie danych w sekcji published — takie dane będą dostępne dla inspektora obiektów.

Przykładowa deklaracja klasy z użyciem sekcji może wyglądać następująco:

TEngine = class
  private
    FFileName : String;
    FFileLines : TStringList;
  protected
    procedure Execute(Path : String);
  public
    Pattern : TTemplate;
    Replace : TTemplate;
    procedure Parse;
    constructor Create(FileName : String);
    destructor Destroy; override;
  end;

Jak widać, wystarczy wpisać odpowiednie słowo kluczowe w klasie i poniżej można wpisywać metody.

Sekcja private

Metody umieszczone w sekcji private są określane jako prywatne. Oznacza to, że nie będą dostępne na zewnątrz modułu, w którym znajduje się dana klasa. A zatem po próbie odwołania się do metody umieszczonej w sekcji private kompilator zasygnalizuje błąd — nazwa owej metody nie będzie mogła być przez niego rozpoznana.

Metoda z sekcji private nie jest dostępna tylko w przypadku, gdy próba odwołania do tej metody następuje z poziomu innego modułu.

Sekcja protected

Metoda umieszczona w sekcji protected jest dostępna zarówno dla modułu, w którym znajduje się klasa, jak i dla całej klasy. Jest to jakby drugi poziom ochrony, gdyż metody z sekcji protected są dostępne dla innych klas, które dziedziczą po danej klasie! Aby to zrozumieć, należy wiedzieć, czym jest dziedziczenie w praktyce — zajmiemy się tym w dalszej części rozdziału.

Sekcja public

Metody umieszczone w sekcji public są dostępne dla wszystkich innych klas i modułów. W tej sekcji powinny znajdować się konstruktory oraz destruktory, a także metody służące do komunikowania się ze „światem zewnętrznym” — np. OdpalSilnik, Stop, Start itp.

Dodatkowe oznaczenia w .NET

Wraz z przystosowaniem Delphi do .NET w skład języka weszły nowe klauzule określające dostęp do klas strict private oraz strict protected. Jeszcze bardziej ograniczają one osiągalność pól i metod — do tego stopnia, że są one dostępne jedynie w obrębie danej klasy — nigdzie indziej. Jest to jak do tej pory najniższy poziom dostępu w programowaniu obiektowym w Delphi.

Oto fragment kodu:

type
  TMoja = class
    strict private
      procedure Main;
  end;

var
  Moja : TMoja;
begin
  Moja := TMoja.Create;
  Moja.Main;  // <-- tu jest błąd
end;

Powyższy przykład pokazuje niemożność uzyskania dostępu do metody Main z klasy TMoja. Kompilator zgłosi błąd: [Error] WinForm2.pas(101): Cannot access private symbol TMoja.Main. Wszystko dzięki temu, że metoda Main znalazła się w sekcji strict private, do której dostęp spoza klasy jest zabroniony.

Nie można używać słowa kluczowego strict w połączeniu z public albo z published. Taki błąd spowoduje wyświetlenie komunikatu: [Error] WinForm2.pas(93): PRIVATE or PROTECTED expected.

Poziomy strict private oraz strict protected zostały wprowadzone ze względu na przystosowanie Delphi do specyfikacji CLS. Programując aplikacje działające dla Win32 w Delphi 2005, projektant także może używać tych poziomów dostępu.

Dziedziczenie

Mechanizmem, który stanowi o potędze programowania obiektowego, jest dziedziczenie. Technika ta umożliwia budowanie klas na podstawie już istniejących. Dzięki temu wiele ważnych metod można umieścić jedynie w klasie bazowej — inne klasy, które po niej dziedziczą, nie muszą ponownie zawierać tych samych funkcji.

Przypatrzmy się źródłu formularza WinForms. Można tam znaleźć następującą linię:

type
  TWinForm2 = class(System.Windows.Forms.Form)

Oznacza to, że klasa TWinForm2 dziedziczy po klasie System.Windows.Forms.Form. Nazwę dziedziczonej klasy należy wpisać w nawiasie za słowem kluczowym class. Dzięki temu tworzony formularz ma takie właściwości jak @@Location@@ czy @@Text@@, które dziedziczy.

type
  TA = class
  private
    Var1 : String;
  public
    procedure Main;
  end;

  TB = class(TA)
  end;

Warto spojrzeć na powyższy fragment kodu. Po uruchomieniu programu klasa TB będzie zawierała wszelkie metody z klasy TA, nawet te z sekcji private. Dzieje się tak dlatego, że obie klasy znajdują się w jednym module. Gdyby klasę TA umieścić w innym module, to TB nie dziedziczyłaby już metod i pól z sekcji private.

Jeżeli więc programista chce, aby metody z sekcji private nie były dostępne nawet dla klas potomnych, to powinien umieszczać je w sekcji strict private.

To samo dotyczy sekcji protected. Domyślną właściwością tej sekcji było udostępnianie metod klasom potomnym — zarówno jeżeli znajdują się w tym samym, jak i w innym module. Jednak jeśli dane metody znajdą się w sekcji oznaczonej słowem strict protected, nie będą one dostępne nawet dla klasy potomnej.

Klasa domyślna

Nawet jeżeli nie zadeklarujemy klasy bazowej dla swojego obiektu, to Delphi jako domyślną przyjmie TObject w przypadku VCL.NET lub System.Object (obie nazwy są jednoznaczne).

Przeciążanie metod

Ciekaw jestem, czy Czytelnik pamięta, jak w rozdziale 3. mówiłem o przeciążaniu procedur i funkcji? Taki sam mechanizm można stosować w przypadku klas. Ponownie jest to związane z dziedziczeniem. Załóżmy, że w klasie A znalazła się metoda Power obliczająca potęgę dowolnej liczby. Parametrem tej funkcji jest liczba typu Integer. Chcemy teraz rozszerzyć funkcjonalność tej klasy, umożliwiając obliczanie potęgi na podstawie parametru Real. Przyjrzyj się dwóm klasom:

type

  A = class
    function Power(Value : Integer) : Integer; overload;
  end;

  B = class(A)
    function Power(Value : Real) : Real; overload;
  end;

Klasa B dziedziczy po klasie A funkcję Power, a dodatkowo wprowadza swoją funkcję, tyle że różniącą się parametrami. Teraz można wywołać konstruktor klasy B i używać bezproblemowo dwóch funkcji:

var
  KlasaB : B;
begin
  KlasaB := B.Create;
  KlasaB.Power(2.2);
  KlasaB.Free;
end;

W rzeczywistości ten przykład jest troszeczkę „naciągany”, ale chodziło mi o wyjaśnienie ogólnej idei, a przykład z obliczaniem potęgi wydaje mi się właściwy. Najważniejsze jest, że wystarczyło zadeklarować jedynie funkcję z parametrem Real i obliczanie potęgi było równie możliwe zarówno w przypadku podania jako parametru liczb: 2, 2.0, jak i 2.2.

Typy metod

Zadeklarowane procedury i funkcje w klasie domyślnie stają się metodami statycznymi. Nie są one opatrzone żadną klauzulą — jest to domyślny typ metod. Możliwe jest jednak tworzenie metod wirtualnych oraz dynamicznych. Wiąże się to z użyciem wobec danej metody klauzuli virtual lub dynamic.

  TEngine2 = class(TEngine)
    procedure A; // statyczna
    procedure B; virtual; // wirtualna
    procedure C; dynamic; // dynamiczna
  end;

Wspominam o tym, gdyż z typami metod wiąże się jeszcze jedno pojęcie, a mianowicie przedefiniowanie metod — o tym będzie mowa w kolejnym podrozdziale.

Metody wirtualne a metody dynamiczne

W działaniu metody dynamiczne i wirtualne są praktycznie takie same. Jedyne co je różni, to sposób wykonywania. Otóż metody wirtualne cechuje większa szybkość wykonania kodu, natomiast metody dynamiczne umożliwiają lepszą optymalizację kodu.

Jak to wygląda od strony kompilatora? Otóż Delphi utrzymuje dla każdej klasy tzw. tablicę VMT (ang. Virtual Method Table). Tablica VMT przechowuje w pamięci adresy wszystkich metod wirtualnych, tak więc niejako przyczynia się do zwiększenia pamięci operacyjnej, jakiej wymaga nasza aplikacja. Można to ominąć, stosując metody dynamiczne, ale — jak mówiłem — wiąże się to z gorszą optymalizacją kodu.

Generalnie zaleca się używanie metod wirtualnych zamiast metod dynamicznych.

Metody abstrakcyjne

Metody abstrakcyjne są takimi metodami, które nie posiadają implementacji — jedynie deklaracje w klasie. I wystarczy, że Czytelnik zapamięta to zdanie. Deklarowanie metod abstrakcyjnych wiąże się z dziedziczeniem — projektant klasy bazowej zaznacza, że klasy potomne muszą zawierać metodę o danej nazwie.

Przykład: jeden programista w zespole tworzy klasę TBasePlayer, w której definiuje abstrakcyjną metodę Play (metoda jest w klasie, ale w kodzie nie znajduje się jej implementacja). Drugi programista tworzy klasę TSpiderMan, która dziedziczy po TBasePower i która będzie musiała zawierać metodę Play, jednak we wszystkich klasach potomnych metoda ta może wyglądać inaczej.

Metodę abstrakcyjną definiuje się z użyciem słowa kluczowego abstract:

  TBasePlayer = class
  // jedynie definicja metody
    procedure Play; virtual; abstract;
  end;

Taki kod zostanie skompilowany bez problemu, mimo że kod nie zawiera implementacji metody Play. Uwaga! Metoda abstrakcyjna musi być jednocześnie wirtualna lub dynamiczna (nie może być statyczna), w przeciwnym przypadku kompilator wyświetli komunikat o błędzie: [Error] WinForm2.pas(38): Abstract methods must be virtual or dynamic.

Teraz można zadeklarować klasę potomną, która musi zawierać metodę Play:

  TSpiderMan = class(TBasePlayer)
  end;

Jeżeli jej nie zadeklarujemy, podczas próby utworzenia instancji klasy kompilator poinformuje o błędzie ([Error] WinForm2.pas(103): Constructing instance of 'TSpiderMan' containing abstract method 'TBasePlayer.Play'):

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Player : TSpiderMan;
begin
  Player := TSpiderMan.Create;
  Player.Free;
end;

Możemy w tym momencie zadeklarować metodę Play w klasie TSpiderMan:

  TSpiderMan = class(TBasePlayer)
    procedure Play; override;
  end;

Jak będzie wyglądać implementacja klasy TSpiderMan? To już zależy od programisty:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Player : TSpiderMan;
begin
  Player := TSpiderMan.Create;
  Player.Play;
  Player.Free;
end;

{ TSpiderMan }

procedure TSpiderMan.Play;
begin
  MessageBox.Show('Metoda Play z klasy TSpiderMan');
end;

Uogólniając, trzeba zapamiętać, że metody abstrakcyjne nie posiadają implementacji.

Przedefiniowanie metod

Pojęcie przedefiniowania metod dotyczy jedynie klas, a konkretnie jest związane z ich dziedziczeniem. Klasa, która dziedziczy po innych klasach, przejmuje ich metody, ale te metody można w „nowej wersji klasy” unowocześniać lub całkowicie zmieniać.
W programie zadeklarowałem trzy klasy:

type
  TFiat = class
    procedure Jedź;
  end;

  TMaluch = class(TFiat)
    procedure Jedź;
  end;

  TFiat125 = class(TFiat)
    procedure Jedź;
  end;

Klasa TFiat jest klasą główną, natomiast TMaluch oraz TFiat125 są jej potomkami. Każda z nich posiada jednak metodę o tej samej nazwie. Implementacja tych metod jest dosyć prosta:

{ TFiat125 }

procedure TFiat125.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TFiat125');
end;

{ TFiat }

procedure TFiat.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TFiat');
end;

{ TMaluch }

procedure TMaluch.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TMaluch');
end;

Warto zastanowić się, jaki będzie rezultat wywołania poniższego kodu:

var
  Klasa : TFiat;
begin
  Klasa := TMaluch.Create;
  Klasa.Jedź;
  Klasa.Free;
end;

Zwróćmy uwagę na to, że zmienna Klasa wskazuje na typ TFiat, ale wywoływany jest konstruktor klasy TMaluch. Jest to możliwe tylko w przypadku dziedziczenia, kiedy klasy są powiązane. Po uruchomieniu takiego programu w oknie zostanie wyświetlony napis: Metoda Jedź z klasy TFiat, gdyż Klasa wskazuje na klasę TFiat.

Istnieje również możliwość zmiany znaczenia metod znajdujących się w klasach bazowych bądź rozszerzenia ich funkcjonalności. Aby tego dokonać, należy właśnie oznaczyć metodę jako wirtualną lub dynamiczną — statyczne metody nie mogą być przedefiniowane.

Wystarczy teraz nieco zmodyfikować deklarację klas do następującej postaci:

type
  TFiat = class
    procedure Jedź; virtual;
  end;

  TMaluch = class(TFiat)
    procedure Jedź; override;
  end;

  TFiat125 = class(TFiat)
    procedure Jedź;
  end;

Klauzula override w klasie TMaluch wiąże się z przykrywaniem metody Jedź. Po wprowadzeniu takich poprawek i ponownym uruchomieniu programu w okienku informacyjnym zostanie wyświetlony napis: Metoda Jedź z klasy TFiat.

W zaprezentowanym przykładzie pokazano właśnie zjawisko polimorfizmu. Metoda o tej samej nazwie może mieć zupełnie inne znaczenie w powiązanych ze sobą klasach. Projektanci klas bazowych często oznaczają metody dyrektywą virtual, aby możliwe było rozszerzenie funkcjonalności metody w klasach potomnych.

Reintrodukcja metody

Przypatrzmy się kolejnemu przykładowi:

type
  TFiat = class
    procedure Jedź; virtual;
  end;

  TMaluch = class(TFiat)
    procedure Jedź;
  end;

W klasie bazowej TFiat metoda Jedź jest opatrzona klauzulą virtual, tak więc jest to metoda wirtualna. Klasa TMaluch posiada metodę o tej samej nazwie, tak więc kompilator wygeneruje podczas kompilacji ostrzeżenie: [Warning] WinForm2.pas(97): Method 'Jedź' hides virtual method of base type 'TFiat'. Delphi próbuje w tym momencie powiedzieć, że programista przykrywa oryginalną metodę z klasy bazowej, nie stosując słowa kluczowego override, a tym samym bez przedefiniowania metody. Można pozbyć się tego komunikatu, wykorzystując klauzulę reintroduce:

  TMaluch = class(TFiat)
    procedure Jedz; reintroduce;
  end;

Słowo kluczowe inherited

Podczas omawiania konstruktorów zastosowałem w którymś z przykładów słowo kluczowe inherited. Polecenie inherited można wstawić w kodzie procedury dziedziczącej po jakiejś klasie. Powoduje ono uruchomienie danej metody z klasy bazowej.
Zmianą w .NET jest to, że w przypadku konstruktora konieczne jest umieszczenie w ciele procedury słowa inherited.
Zmodyfikujmy więc metodę Jedź z klasy TMaluch do następującej postaci:

procedure TMaluch.Jedź;
begin
  inherited;
  MessageBox.Show('Metoda Jedź z klasy TMaluch');
end;

Po uruchomieniu programu kompilator, napotykając na słowo inherited, najpierw szuka metody Jedź w klasie bazowej TFiat i wykonuje ją. Dopiero później powraca z powrotem do wykonywania dalszej części kodu. Efektem tego jest wyświetlenie dwóch okien — najpierw z napisem Metoda Jedź z klasy TFiat, a później Metoda Jedź z klasy TMaluch.

Czasami zaistnieje konieczność podania nazwy innej metody, która ma zostać wywołana w danym momencie. Czytelnik mógł zaobserwować to we wcześniejszych przykładach, gdy konieczne było wywołanie konstruktora. Pisałem wtedy tak:

inherited Create;

Wszystko dlatego, że samo słowo inherited wywołuje metodę z klasy bazowej o tej samej nazwie, w której się znajduje. Tak więc, jeżeli napiszę:

procedure TMaluch.Jedź;
begin
  inherited;
end;

kompilator wywoła metodę Jedź z klasy bazowej TFiat. Przypatrzmy się jednak temu fragmentowi kodu:

type
  TFiat = class(System.Object)
    procedure Jedź; virtual;
  end;

  TMaluch = class(TFiat)
    procedure Jedziemy;
  end;

{ TFiat }

procedure TFiat.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TFiat');
end;

{ TMaluch }

procedure TMaluch.Jedziemy;
begin
  inherited Jedź;
  MessageBox.Show('Metoda Jedziemy z klasy TMaluch');
end;


procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Klasa : TMaluch;
begin
  Klasa := TMaluch.Create;
  Klasa.Jedziemy;
  Klasa.Free;
end;

W klasie TMaluch zmieniłem nazwę metody z Jedź na Jedziemy. Jeżeli w nowej metodzie wpiszę inherited, to kompilator będzie szukał metody Jedziemy w klasie bazowej TFiat — i nie znajdzie takiej. Należy więc podać mu szczegółowe namiary na metodę, którą chcemy wywołać — inherited Jedź;.

Inherited w konstruktorach

Delphi dla .NET wymaga, aby w konstruktorach klas umieszczać słowo kluczowe inherited (co nie jest konieczne w środowisku Win32). Zwyczajnie jest to wymóg .NET, aby przed uzyskaniem dostępu do członków danej klasy, wywoływać konstruktor klasy bazowej. W tym celu taki konstruktor powinien wyglądać następująco:

constructor TFiat.Create;
begin
  inherited Create;
  { kod }
end;

Zawsze najlepiej obok samego słówka inherited dodawać nazwę metody — w tym przypadku Create. Może się bowiem zdarzyć, że nasz konstruktor będzie miał inną nazwę niż konstruktor w klasie bazowej:

constructor TFiat.CreateMain;
begin
  inherited Create;
  { kod }
end;

W takim przypadku niewystarczające jest samo słowo kluczowe inherited.

Jeżeli konstruktor nie zawiera słowa kluczowego inherited, kompilator wskaże błąd: [Error] WinForm2.pas(108): 'Self' is uninitialized. An inherited constructor must be called.

Typy zagnieżdżone

W języku Delphi (zarówno dla .NET, jak i Win32) istniej możliwość umieszczania klas w klasie, tak samo jak w przypadku zmiennych, stałych i procedur. Oto przykład:

type
  TFiat = class
    procedure Jedź; virtual;

    type
      TMaluch = class
        procedure Jedź;
      end;
  end;

Klasa zagnieżdżona działa identycznie jak zwykła. Może tak samo zawierać w sobie metody oraz pola, ba, może nawet zawierać w sobie inne zagnieżdżone klasy. Jedyna różnica, o jakiej trzeba pamiętać, to deklaracja metody z klasy zagnieżdżonej w sekcji implementation:

procedure TFiat.TMaluch.Jedź;
begin

end;

Skoro klasa TMaluch należy do klasy TFiat, to jej budowa będzie charakterystyczna, tak samo jak wywołanie konstruktora:

var
  Klasa : TFiat.TMaluch;
begin
  Klasa := TFiat.TMaluch.Create;
  Klasa.Jedź;
  Klasa.Free;
end;

Wystarczy zapamiętać i nauczyć się deklaracji nowych klas, a klasy zagnieżdżone nie powinny stanowić większego problemu.

W bibliotece klas platformy .NET (FCL) nie stosuje się specjalnego, charakterystycznego dla Delphi nazewnictwa polegającego na umieszczaniu przed nazwą klasy litery T.

Parametr Self

Słowo kluczowe Self jest wskaźnikiem klasy. Jest to niewidoczny z punktu widzenia programisty element języka. Oto przykładowa procedura zdarzeniowa Load projektu Windows Forms:

procedure TWinForm2.TWinForm2_Load(sender: System.Object; e: System.EventArgs);
begin
  Text :=  'Nowa wartość';
end;

W takim przypadku Delphi określa, że chodzi tutaj o odwołanie do właściwości @@Text@@ klasy TWinForm2 i kod jest wykonywany prawidłowo. Równie dobrze można by napisać:

  Self.Text := 'Nowa wartość';

Z punktu widzenia kompilatora wygląda to tak jak powyżej, czyli przy użyciu wskaźnika Self na daną klasę — kod taki również zostanie skompilowany prawidłowo.

Słowa kluczowego Self nie stosuje się zbyt często, warto jednak wiedzieć, że istnieje taki mechanizm jak wskazanie na klasę. Praktyczne zastosowanie Self zostanie opisane w rozdziale 11., gdzie pokażę, w jaki sposób można utworzyć komponent w trakcie działania programu.

Brak konstruktora

W pewnych sytuacjach obecność konstruktora oraz destruktora jest zbędna. Podczas wywoływania konstruktora Delphi alokuje pamięć dla zmiennych potrzebnych i wykorzystywanych przez klasę. Egzemplarzy klasy może być wiele, stąd w pamięci istnieje kilka kopii zmiennych klasowych (oczywiście każda może zawierać inne dane). Jeżeli nie zostanie wywołany konstruktor, dostęp do zmiennych klasowych jest zabroniony i skończy się nieprawidłowym zachowaniem programu (rysunek 7.1).

7.1.jpg
Rysunek 7.1. Błąd występujący na skutek niewywołania konstruktora

Przyjrzyjmy się poniższemu fragmentowi kodu:

type
  TFiat = class
    procedure Jedź; virtual;

    type
      TMaluch = class
        Zmienna : Boolean;  
        procedure Jedź;
      end;
  end;

{ TFiat }

procedure TFiat.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TFiat');
end;

{ TFiat.TMaluch }

procedure TFiat.TMaluch.Jedź;
begin
  MessageBox.Show('Metoda Jedź z klasy TFiat.TMaluch');
end;

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  Maluch : TFiat.TMaluch;
begin
  Maluch.Jedź;
end;

Powyższy kod zostanie wykonany bezproblemowo, ponieważ w Delphi zapewnia dostęp do metod klasy bez wywoływania konstruktora, lecz niemożliwe jest przydzielanie danych zmiennym klasowym (czyli nie można przypisać wartości polom klasy).

Przed skompilowaniem programu Delphi wyświetli ostrzeżenie, że nie został wywołany konstruktor klasy: [Warning] Uni1.pas(125): Variable 'Maluch' might not have been initialized. Nie należy stosować takiej praktyki! Mimo że program działa dobrze, zaleca się zawsze wywoływanie konstruktora oraz destruktora klasy. Pozwoli to na wyeliminowanie ewentualnych błędów związanych z brakiem konstruktora.

Brak instancji klasy

W niektórych przypadkach nie jest nawet konieczne deklarowanie zmiennej wskazującej na daną klasę — można po prostu wywołać metodę znajdującą się w niej w poniższy sposób:

TFiat.Jedź;

Taka konstrukcja nosi nazwę simple class methods (z ang. proste metody klasy). Kod ten też zadziała, pod warunkiem, że metoda Jedź zostanie zadeklarowana w odpowiedni sposób — ze słowem class na początku:

type
  TFiat = class
    procedure Jedź; virtual;

    type
      TMaluch = class
      public
        class var Zmienna : Boolean;
        class procedure Jedź;
      end;
  end;

Wówczas dostęp do klasy może wyglądać następująco:

begin
  TFiat.TMaluch.Zmienna := True;
  TFiat.TMaluch.Jedź;
end;

Należy jednak pamiętać o specyficznej deklaracji pola lub metody:

        class var Zmienna : Boolean; // zwróć uwagę na słówko var
        class procedure Jedz;

Taka instrukcja też nie jest zalecana — pozwala jednak na szybkie użycie klasy bez wywoływania jej konstruktora, a nawet bez inicjalizacji obiektu wskazującego na ową klasę.

Jeżeli procedura została zadeklarowana z klauzulą class, to jej definicja również musi ją zawierać:

class procedure TSamochodzik.Jedz;
begin
  MessageBox.Show('Jedz z klasy TSamochodzik');
end;

Również w przypadku, gdy metoda jest wirtualna — klasa potomna także musi zawierać klauzulę class (przykład poniżej):

type
  TFiat = class
    procedure Jedź; virtual;

    type
      TMaluch = class
      public
        class var Zmienna : Boolean;
        class procedure Jedź; virtual;
      end;
  end;

  TSamochodzik = class(TFiat.TMaluch)
  public
    class procedure Jedź; override;
  end; 

Jak wspominałem, aby skorzystać z metod prostych, nie jest potrzebna zmienna wskazująca na klasę. Możliwe jest jednak wykorzystanie takich metod oraz zmiennych po zainicjalizowaniu klasy:

var
  Maluch : TFiat.TMaluch;
begin
  Maluch := TFiat.TMaluch.Create;
  Maluch.Zmienna := False;
  Maluch.Jedź;
  Maluch.Free;
end; 

Przestrzegam jednak w tym miejscu przed pominięciem wywołania konstruktora. Próba uzyskania dostępu do zmiennej lub do metody w przypadku pominięcia konstruktora może się skończyć błędem aplikacji.

Metody zadeklarowane jako simple class methods nie będą dostępne na liście Code Completion w przypadku próby odwołania poprzez obiekt (tak jak to ma miejsce w powyższym kodzie źródłowym).

Z ciała metody, która jest metodą prostą, można uzyskać dostęp do elementów klasy. Innymi słowy — dostępna jest zmienna @@Self@@, tak więc możemy zarówno odczytywać, jak i przypisywać dane polom, a także wywoływać inne metody, pod warunkiem, że będą to metody proste.

Proste metody klasy to element dostępny tylko dla języka Delphi dla .NET.

Class helpers

Innowacją wprowadzoną w Delphi 8 jest mechanizm class helpers, umożliwiający rozszerzenie funkcjonalności danej klasy bez konieczności dziedziczenia po niej, czy też wprowadzania jakiejkolwiek zmiany w jej kodzie.

Mechanizm class helpers charakteryzuje się specyficzną konstrukcją:

  TFiat = class
    KM : Integer;
  end;

  TFiatHelper = class helper for TFiat
    procedure Jedź;
  end;

Przykład ten prezentuje utworzenie klasy TFiatHelper, która prezentuje właśnie mechanizm class helpers, czyli rozszerzenie funkcjonalności klasy TFiat. Oto schemat budowy takiej klasy:

NazwaKlasy = class helpers for [KlasaBazowa];
end;

W miejscu [KlasaBazowa] należy wstawić nazwę klasy, której funkcjonalność ma zostać rozszerzona. Same dodawanie metod i właściwości odbywa się w ten sam sposób co w zwykłej klasie. Spójrzmy na procedurę Jedź z klasy TFiatHelper:

procedure TFiatHelper.Jedź;
begin
  KM := 25;
  MessageBox.Show(Convert.ToString(KM));
end;

Warto zwrócić uwagę, że możemy się bez problemu odwołać do właściwości znajdującej się w klasie TFiat (właściwość @@KM@@).
Dzięki class helpers można odwołać się do metody Jedź (która znajduje się w klasie TFiatHelper) z klasy TFiat:

var
  Fiat : TFiat;
begin
  Fiat := TFiat.Create;
  Fiat.Jedź;
end;

Z pozoru dla class helpers nieistotne są sekcje klasy bazowej. Przykładowo, jeżeli zmienna KM znajdzie się w sekcji strict private, to i tak program zostanie skompilowany, lecz w trakcie działania taki kod spowoduje pojawienie się błędu. Podsumowując, metody i pola w klasie bazowej nie mogą znajdować się w sekcji strict private oraz strict protected, jeżeli class helpers ma mieć do nich dostęp.

Klasy typu class helpers mogą zawierać sekcje private/public/protected, konstruktory, metody wirtualne oraz dynamiczne, a także zmienne wskazujące na dany obiekt.

Klasy zaplombowane

W Delphi istnieje możliwość określenie klasy jako „zaplombowanej”. Uniemożliwia to ewentualne późniejsze dziedziczenie po tej klasie oraz rozszerzanie jej funkcjonalności. W takim przypadku należy oznaczyć klasę klauzulą sealed:

  TMyClass = class sealed
  { metody }
  end;

Teraz, jeśli jakaś klasa miałaby dziedziczyć po klasie TMyClass, Delphi wyświetli komunikat o błędzie: [Error] WinForm2.pas(40): Cannot extend sealed class 'TMyClass'.

Słowo sealed umieszcza się zawsze przed nazwą klasy, po której ewentualnie będą dziedziczyć inne klasy, np.:

  TMyClass = class sealed (System.Windows.Forms.Form)
  { metody }
  end;

Klasa zaplombowana nie można zawierać metod abstrakcyjnych!

Słowo kluczowe static

Podobnie jak w przypadku metod prostych (dla przypomnienia: tych, w których definicja jest poprzedzona słowem kluczowym class), metody opatrzone słowem kluczowym static zapewniają dostęp do klasy bez konieczności wywoływania konstruktora czy odwoływania się do instancji klasy.

Dyrektywa static była nowością w Delphi 8 zapewnioną przez .NET. Metody oznaczone słowem static nie mogą odwołać się do jakiegokolwiek elementu (członka) klasy — nie posiadają więc zmiennej @@Self@@. Oto fragment kodu:

type
  TMyClass = class
  public
    class var S : String;
    class procedure SetValue(const Value : String); static;
    class procedure Normal;
  end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  TMyClass.S := 'dane';
  TMyClass.SetValue('Wartość');
end;

{ TMyClass }

class procedure TMyClass.Normal;
begin
 { kod }
end;

class procedure TMyClass.SetValue(const Value: String);
begin
  S := Value;
{ Zmienna S będzie posiadała wartość przekazaną w parametrze }
end;

Metoda SetValue jest oznaczona dyrektywą static. Oznacza to, że nie zadeklarowano zmiennej @@Self@@ i że SetValue nie może odwoływać się do członków klasy TMyClass, jeżeli metody nie są zadeklarowane z użyciem słowa kluczowego class.

Dodatkowo, metody opatrzone słowem kluczowym static nie mogą być wirtualne ani dynamiczne — jak mówi sama nazwa, są to metody statyczne, które nie mogą podlegać procesowi przedefiniowania.

Właściwości

Tyle już napisałem o programowaniu obiektowym, lecz temat właściwości pozwoliłem sobie pozostawić na sam koniec.
Właściwości, podobnie jak pola (te pojęcia są często ze sobą mylone) służą do gromadzenia danych (czyli do odczytywania oraz przypisywania informacji). Oprócz tego w przypadku właściwości istnieje możliwość zaprogramowania dodatkowych czynności podczas, na przykład, przypisywania wartości (danych) owej właściwości.

Właściwości są deklarowane z użyciem słowa kluczowego property. Właściwość musi posiadać przede wszystkim nazwę oraz typ (Integer, String itd.). Dodatkowo, właściwość może być opisana słowami kluczowymi read, write, default czy nodefault. Oto przykładowa deklaracja właściwości:

type
  TMyClass = class
  private
    FText : String;
  public
    property Text : String read FText;
  end;

Zacznijmy od tego, że właściwości mogą być zarówno do zapisu, jak i do odczytu; mogą jednakże być tylko do odczytu. W powyższym przykładzie właściwość @@Text@@ jest właściwością tylko do odczytu (ma klauzulę read), a swoją zawartość pobiera z pola @@FText@@.

Właściwość @@Text@@ jest w tym przypadku czymś na wzór łącznika pomiędzy użytkownikiem klasy a polem @@FText@@.

Przy próbie przypisania danych właściwości tylko do odczytu Delphi wyświetli komunikat o błędzie: Cannot assign to a read-only property.

Wspominałem o tym, że istnieje możliwość zaprogramowania dodatkowych czynności związanych z przypisaniem lub odczytywaniem danych z właściwości:

type
  TMyClass = class
  private
    FText : String;
    procedure SetText(Value : String);
  public
    property Text : String read FText write SetText;
  end;

W tym momencie przypisanie danych do właściwości @@Text@@ jest równoznaczne z wywołaniem metody SetText:

var
  MyClass : TMyClass;
begin
  { konstruktor }
  MyClass.Text := 'Wartość';
  { destruktor }
end;

Kod ten jest równoznaczny z:

MyClass.SetText('Wartość');

Wspomniana procedura SetText musi zawsze posiadać parametr, który jest tego samego typu co właściwość. W tym przypadku właściwość @@Text@@ jest typu String, tak więc parametr procedury SetText również musi być typu String. Skoro skorzystaliśmy z funkcji, możemy także użyć funkcji do odczytania wartości właściwości:

type
  TMyClass = class
  private
    FText : String;
    procedure SetText(Value : String);
    function GetText : String;
  public
    property Text : String read GetText write SetText;
  end;

Funkcja GetText będzie wywoływana w momencie żądania użytkownika o odczytanie zawartości @@Text@@. Funkcja GetText musi również zwracać typ String.

W przypadku właściwości stosuje się specjalną konwencję nazewnictwa. Nazwa funkcji i procedury jest tworzona od nazwy właściwości. Jedyną różnicą jest dodanie przedrostków Set oraz Get. I tak w przypadku właściwości @@Text@@ mamy funkcję GetText oraz procedurę SetText. Nie jest to oczywiście obowiązek, ale taka reguła została przyjęta przez ogół programistów.

Procedura zastosowana w połączeniu z właściwością może być wykorzystana do różnych celów. W ciele takiej procedury mogą znaleźć się inne instrukcje, które — przykładowo — mają zostać wykonane w momencie przypisania danych do właściwości. W tej materii istnieje kilka reguł, do których trzeba się stosować:

*Funkcje i procedury użyte w połączeniu z właściwością muszą być zadeklarowane w tej samej klasie.
*Niezależnie od tego, czy jest to pole, funkcja czy procedura — wszystkie one muszą być tego samego typu lub mieć te same parametry.
*Metody połączone z właściwościami nie mogą być opatrzone klauzulą virtual lub dynamic — nie mogą być dziedziczone ani przeładowane.

Właściwości bardzo często są wykorzystywane podczas tworzenia nowych komponentów. VCL jest biblioteką skalowaną, tzn. ma możliwość łatwego rozbudowywania. Na podstawie istniejących już klas lub kontrolek można budować nowe. W takim przypadku, używając klasy, można użyć jeszcze jednej sekcji, o której dotychczas nie wspominałem — published. W sekcji published w klasie znajdują się przeważnie właściwości, które będą dostępne dla inspektora obiektów, bowiem tworzenie nowego komponentu w dużej mierze opiera się na tworzeniu odpowiedniej klasy. W niniejszej książce nie będę omawiał tworzenia nowych komponentów. Jeżeli Czytelnik jest zainteresowany taką tematyką, odsyłam do książki Delphi 7. Kompendium programisty wydanej nakładem wydawnictwa Helion w 2003 roku.

Wartości domyślne

Wspominałem na samym początku, iż właściwości mogą mieć klauzule default lub nodefault. Służą one po prostu nadawaniu domyślnych wartości danym właściwościom.

Oto przykład nadania wartości domyślnej właściwości Count:

property Count : Integer read FCount write FCount default 1;

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

Klauzula default obejmuje jedynie typy liczbowe i zbiory (set) — nie obsługuje natomiast ciągów znakowych String.

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 jako domyślną wartość właściwości przyjmie True.

Klauzula nodefault jest przeciwieństwem defulat — oznacza, że właściwość nie będzie miała wartości domyślnej. Klauzula ta jest stosowana jedynie w niektórych przypadkach, gdy klasa bazowa, z której korzysta dana klasa, ma 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 przypadku klasa TMyClass także będzie mieć właściwość @@Prop@@, tyle że nie będzie już ona zawierała wartości domyślnej.

Parametr Sender procedury zdarzeniowej

Spostrzegawczy Czytelnik mógł zauważyć, że po wygenerowaniu procedury zdarzeniowej komponentu zawsze ma ona parametr @@Sender@@. Przykładowo, otwórzmy projekt WinForms i umieśćmy na formularzu komponent TextBox (odpowiednik TEdit z VCL.NET). Odszukajmy na zakładce Events zdarzenie KeyDown i wygenerujmy je.
Teraz powinno nastąpić uruchomienie edytora kodu — Delphi automatycznie umieści w kodzie odpowiednią procedurę:

procedure TWinForm2.TextBox1_KeyDown(sender: System.Object; e: System.Windows.Forms.KeyEventArgs);
begin

end;

Zdarzenie KeyDown odpowiada za pobieranie informacji dotyczących klawisza naciśniętego podczas działania programu w obrębie danej kontrolki (w tym wypadku TextBox). Ma ono dwa parametry — @@Sender@@ i @@E@@. Parametr @@E@@ zawiera informacje o klawiszu, który został naciśnięty podczas działania aplikacji.

Parametr @@Sender@@ jest jakby „wskaźnikiem” — dzięki niemu możemy się dowiedzieć, z jakiego komponentu pochodzi zdarzenie, co jest ważne w przypadku, gdy jedna procedura obsługuje kilka zdarzeń jednego typu. Aby lepiej to zilustrować, napiszemy odpowiedni program.

Przechwytywanie informacji o naciśniętym klawiszu

Celem tego ćwiczenia jest napisanie programu, który będzie w określony sposób reagował na wciśnięcie, przytrzymanie oraz puszczenie klawisza. Odpowiadają za to zdarzenia: KeyDown, KeyPress oraz KeyUp.

#Otwórz nowy projekt Windows Forms.
#Umieść na formularzu komponent RichTextBox.
#Umieść na formularzu komponent TextBox.
#Wygeneruj trzy zdarzenia komponentu TextBoxKeyDown, KeyUp oraz KeyPress.

Działanie programu będzie dość proste. Użytkownik, naciskając odpowiednie klawisze w obrębie kontrolki TextBox, spowoduje wywołanie zdarzeń, które w kontrolce RichTextBox wyświetlą informację o klawiszu.

Zdarzenie KeyDown występuje w momencie naciśnięcia klawisza przez użytkownika. Zdarzenie to będzie zachodziło, dopóki użytkownik nie puści tego klawisza. Umożliwia ono także przechwytywanie naciskania takich klawiszy jak F1…F12, Home, End itd.

Zdarzenie KeyPress zachodzi w trakcie naciskania klawisza na klawiaturze. W przypadku, gdy użytkownik naciśnie taki klawisz jak Alt lub Ctrl, zdarzenie to nie występuje.

Zdarzenie KeyUp natomiast jest generowane w momencie puszczenia naciśniętego uprzednio klawisza klawiatury. Ponadto zdarzenia KeyDown i KeyUp dostarczają informacji o tym, czy w danym momencie jest także wciśnięty klawisz Ctrl lub Alt czy może jest naciśnięty lewy przycisk myszy.

procedure TWinForm2.TextBox1_KeyUp(sender: System.Object; e: System.Windows.Forms.KeyEventArgs);
begin
  RichTextBox1.AppendText('Puszczono klawisz #' + Convert.ToString(E.KeyValue) + #13);
end;

procedure TWinForm2.TextBox1_KeyPress(sender: System.Object; e: System.Windows.Forms.KeyPressEventArgs);
begin
  RichTextBox1.AppendText('Nacisnięto klawisz ' + E.KeyChar + #13);
end;

procedure TWinForm2.TextBox1_KeyDown(sender: System.Object; e: System.Windows.Forms.KeyEventArgs);
begin
  RichTextBox1.AppendText('Wcisnięto klawisz #' + Convert.ToString(E.KeyValue) + #13);
end;

Wspomniałem już o parametrze @@E@@, który wskazuje na klasę System.Windows.Forms.KeyEventArgs. Dzięki temu parametrowi możemy odczytać kod ASCII znaku, jaki został naciśnięty przez użytkownika na klawiaturze.

ASCII (skrót od ang. American Standard Code for Information Interchange) jest zestawem kodów, standardowo z zakresu 0 – 127 (dziesiętnie), przyporządkowany przez ANSI (amerykański instytut ds. standardów) poszczególnym znakom alfanumerycznym (litery alfabetu angielskiego i cyfry), znakom pisarskim oraz sterującym (np. znak nowego wiersza). Przykładowo, litera „a” jest zakodowana liczbą 67.

W ten sposób właściwość KeyValue (liczba Integer) dostarcza kod znaku ASCII, który należy wyświetlić w kontrolce RichTextBox.

Zupełnie inna sytuacja zachodzi w przypadku zdarzenia KeyPress — tam parametr @@E@@ wskazuje na klasę KeyPressEventArgs, która zawiera właściwość @@KeyChar@@ — klawisz, który został naciśnięty (w postaci typu Char).

Wyjaśnić należy również sposób, w jaki dodaje się nowy tekst do komponentu RichTextBox. Realizuje to metoda AppendText, której jedynym argumentem jest dodawany tekst.

Warto też zwrócić uwagę na dziwne zapisy w powyższym przykładzie. Chodzi mu tutaj o specyficzny wpis — #13. Kod numer 13 w standardzie ASCII odpowiada znakowi Enter, natomiast symbol # oznacza przekształcenie nowego numeru na postać znakową. Dzięki temu teksty dodawane do komponentu RichTextBox są wyświetlane linia po linii.

Można już skompilować i uruchomić aplikację — rezultat jej działania został przedstawiony na rysunku 7.2.

7.2.jpg
Rysunek 7.2. Program w trakcie działania

Obsługa zdarzeń przez inne komponenty

Przeznaczenie napisanego przed chwilą programu jest raczej nieokreślone. Teraz, aby zaprezentować działanie parametru @@Sender@@, należy dodać do formularza jeszcze jeden komponent. Może to być nawet komponent CheckBox, gdyż również posiada zdarzenia KeyDown, KeyUp i KeyPress. Umieśćmy go na formularzu oraz oprogramujmy procedury zdarzeniowe komponentu TextBox do CheckBox (o tym mówiłem na początku tego rozdziału). Dzięki temu dwie kontrolki obsługują te same zdarzenia.

Obsługa parametru Sender

Trzeba wiedzieć, że w systemie takim jak Windows kontrolki mogą przybierać stany aktywny oraz nieaktywny. Przykładowo, komponent TextBox jest aktywny, gdy jest zaznaczony (w jego polu tekstowym miga kursor). Jeżeli umieściliśmy już na formularzu komponent CheckBox, można ponownie uruchomić program. Po kliknięciu myszą w obrębie owego komponentu, zostanie on zaznaczony. Teraz, gdy naciskamy jakiś klawisz, w rzeczywistości są wywoływane zdarzenia, które pierwotnie były zdarzeniami obiektu TextBox.

Mówię o tym, ponieważ parametr @@Sender@@ procedury zdarzeniowej umożliwia programiście kontrolę oraz daje informację, z jakiego komponentu pochodzi zdarzenie. Teraz możemy zmodyfikować zdarzenie KeyPress do następującej postaci:

procedure TWinForm2.TextBox1_KeyPress(sender: System.Object; e: System.Windows.Forms.KeyPressEventArgs);
begin
  RichTextBox1.AppendText('Naciśnięto klawisz ' + E.KeyChar + '. Zdarzenie pochodzi z kontrolki ' + Sender.ClassName + #13);
end;

Właściwość @@ClassName@@ z klasy System.Object daje informacje w postaci ciągu znakowego (typ String), dotyczące nazwy klasy (rysunek 7.3).

7.3.jpg
Rysunek 7.3. Program po modyfikacjach

Operatory is i as

Istnieją dwa operatory, is i as, które są stosowane w połączeniu z klasami. Programiści rzadko z nich korzystają, jednak warto poświęcić im nieco uwagi.

Pierwszy z nich — operator is — służy do sprawdzania, czy np. aktywna kontrolka jest typu TEdit. To jest tylko przykład, gdyż operator ten zazwyczaj realizuje porównanie typów klas — przykładowo:

  if Sender is TextBox then { kod }

W przypadku, gdy zdarzenie pochodzi z komponentu typu TEdit (wskazuje na to parametr @@Sender@@), instrukcja if zostaje spełniona. Operator is działa podobnie jak porównanie za pomocą =. Niekiedy jednak nie można użyć operatora =:

  if Sender = TextBox then { kod }

Powyższy kod spowoduje wyświetlenie komunikatu o błędzie: [Error] MainFrm.pas(126): Operator not applicable to this operand type, gdyż parametr @@Sender@@ pochodzący z klasy System.Object oraz klasa TextBox to dwa oddzielne obiekty.

Operator as natomiast służy do tzw. konwersji. Nie jest to jednak typowa konwersja, jaką omawiałem w rozdziale 3.

Załóżmy, że na formularzu umieściłem kilka kontrolek typu TextBox — zdarzenie KeyPress dla każdej z nich jest obsługiwane przez jedną procedurę zdarzeniową. Chciałbym zmienić jakąś właściwość jednego komponentu typu TextBox, a to jest możliwe dzięki operatorowi as.

procedure TWinForm2.TextBox1_KeyPress(sender: System.Object; e: System.Windows.Forms.KeyPressEventArgs);
begin
  if Sender is TextBox then
  begin
    (Sender as TextBox).Text := '';
  end;
  RichTextBox1.AppendText('Naciśnięto klawisz ' + E.KeyChar + '. Zdarzenie pochodzi z kontrolki ' + Sender.ClassName + #13);
end;

Po uruchomieniu programu i naciśnięciu klawisza w momencie, gdy komponent TEdit jest aktywny, zostanie wywołane zdarzenie OnKeyPress — wówczas właściwość @@Text@@ (która określa tekst wpisany w kontrolce) zostanie wyczyszczona.

Jak widać, dzięki takiemu zabiegowi jest możliwa zmiana właściwości takiego komponentu nawet bez znajomości jego nazwy, a jedynie typu.

Formularz VCL.NET posiada właściwość @@ActiveControl@@, która „ustawia” wybraną, aktywną kontrolkę zaraz po uruchomieniu programu.

Metody w rekordach

Delphi 8 wprowadza pewne innowacje w zapisie rekordów. Dotychczas rekord mógł zawierać jedynie pola (ang. fields), czyli zmienne rekordowe. Teraz jest możliwe także dodawanie metod, co bardziej upodobnia rekordy do klas. Oczywiście, rekordy są prostszą strukturą niż klasy i nie mogą zwierać metod wirtualnych czy dynamicznych. Nie można dziedziczyć rekordów oraz przedefiniowywać metod.

Metody w rekordach deklaruje się identycznie jak w klasach:

  TRecord = record
    X, Y : Integer;
    procedure Main;
  end;

Należy zwrócić uwagę, że jawne wywołanie konstruktora w rekordzie nie jest możliwe. Oczywiście, istnieje możliwość jego deklaracji, ale jego wywołanie następuje automatycznie w momencie skorzystania z metody w rekordzie.

Deklaracja konstruktora musi nastąpić z użyciem słowa kluczowego class, tak jak poniżej:

type
  TRecord = record
    X, Y : Integer;
    procedure Main;
    class constructor Create;
  end;

W razie zaniechania użycia słowa kluczowego class, Delphi wyświetli komunikat o błędzie: [Error] Uni1.pas(110): Parameterless constructors not allowed on record types.

Należy również pamiętać o specyficznej budowie nagłówka metody, tak jak w przypadku klas, czyli:

procedure/function Klasa.Nazwa [parametry]

Spójrzmy na poniższy fragment kodu:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  R : TRecord;
begin
  R.X := 57; // przydzielenie wartości
  R.Main; // wywołanie metody
end;


{ TRecord }

class constructor TRecord.Create;
begin
  MessageBox.Show('Rozpoczynam używanie rekordu TRecord...');
end;

procedure TRecord.Main;
begin
  MessageBox.Show('Mój wiek: ' + Convert.ToString(X));
end;

Powyższy przykład jest oczywiście niezbyt praktyczny i użyteczny, ale pozwala na zaprezentowanie działania rekordów i wywoływania konstruktora. Można wkleić taki kod do swojego programu i zaobserwować jego działanie.

Po naciśnięciu przycisku najpierw w okienku informacyjnym pojawi się tekst: Rozpoczynam używanie rekordu TRecord..., a dopiero później: Mój wiek: 57.

W rekordach nie można używać destruktora. Przy próbie jego wykorzystania Delphi wyświetli komunikat o błędzie: [Error] Uni1.pas(111): PROCEDURE, FUNCTION, PROPERTY, or VAR expected lub [Error] Uni1.pas(111): Unsupported language feature: 'destructor'.

Metody deklarowane w rekordach są domeną języka Delphi dla .NET — kompilator Delphi dla Win32 nie dopuści do kompilacji takiego kodu.

Interfejsy

Interfejsy stanowią specjalną kategorię klas. Są związane z wykorzystaniem modelu COM (ang. Component Object Model). Innymi słowy, interfejs jest zbiorem funkcji i procedur umożliwiających integrację z obiektem COM. Z punktu widzenia języka Delphi interfejs jest klasą pozbawioną pól i posiadającą wyłącznie metody.

Wszystkie interfejsy wywodzą się z klasy IUnknown, podobnie jak VCL.NET wywodzi się z TObject. Interfejs jest deklarowany z użyciem słowa kluczowego interface:

type
  ISomeInterface = interface
  end;

Istnieje kilka istotnych reguł dotyczących interfejsów, które Czytelnik powinien znać:

*Interfejsy mogą zawierać jedynie metody lub właściwości. Pola są zabronione.
*Jako że interfejsy nie mogą zawierać pól, słowa kluczowe read i write muszą wskazywać na metody.
*Wszystkie metody interfejsu są domyślnie publiczne. W interfejsach nie istnieje coś takiego jak dostępność klas.
*Interfejsy nie mają konstruktorów ani destruktorów.
*Metody interfejsu nie mogą być opatrzone dyrektywami virtual, dynamic, abstract lub override.

Istotną sprawą jest fakt, że interfejs nie posiada implementacji swoich metod. Implementacja interfejsów odbywa się poprzez klasy:

type
  ISomeInterface = interface
  ['{50D77FA6-2D7C-45BE-96A2-3B23C38ED6B7}']
    procedure AddRef;
    procedure RemoveRef;
  end;

  TSomeObject = class(TObject, ISomeInterface)
  public
    procedure AddRef;
    procedure RemoveRef;
  end;

{ TSomeObject }

procedure TSomeObject.AddRef;
begin

end;

procedure TSomeObject.RemoveRef;
begin

end;

Implementacja interfejsów poprzez klasy jest uwarunkowana architekturą COM. Interfejsy pośredniczą jedynie w komunikowaniu się obiektów COM z aplikacją, która wykorzystuje ów obiekt. Więcej informacji na ten temat znajduje się w rozdziale 8.

Dobrze byłoby zwrócić uwagę, że klasa TSomeObject dziedziczy zarówno po klasie TObject, jak i po interfejsie ISomeInterface. Metody w klasie muszą być identyczne z tymi w dziedziczonym interfejsie.

Każdy interfejs charakteryzuje tzw. Global Unique Identifier (GUID), czyli 128-bitowy numer identyfikujący dany interfejs. GUID jest generowany w edytorze kodu po wybraniu skrótu klawiaturowego Ctrl+Shift+G.

Przeładowanie operatorów

Mechanizm przeładowania operatorów (lub inaczej — przeciążania operatorów) był znany już wcześniej, m.in. programistom C++, a teraz został wprowadzony również do Delphi.

Mówiąc najogólniej, dzięki przeładowaniu operatorów można dokonywać różnych działań na obiektach klas (mnożenie, dodawanie, dzielenie itp.) tak samo jak na zmiennych, tym samym upraszczając nieco zapis kodu. Wygląda to tak: stosując operator (np. +) w obiektach klas, w rzeczywistości wywołujemy odpowiednią funkcję z klasy. Od projektanta zależy, jaki kod będzie miała owa funkcja. Działanie takie wygląda mniej więcej tak:

var
  X, Y : TMyClass;
begin
  X := 10;
  Y := X + X;
end;

Na początku ten mechanizm może wydawać się trochę dziwny, lecz osoby mające wcześniej styczność np. z C++ nie powinny mieć problemu ze zrozumieniem tego problemu.

Jakie operatory można przeładować?

W Delphi istnieje wiele operatorów, począwszy od operatorów arytmetycznych, a skończywszy na binarnych. O operatorze można powiedzieć także w przypadku funkcji Inc! Odpowiednikiem tego operatora w języku C jest ++, ale to nie zmienia faktu, że można przeładować także Inc.

W celu zadeklarowania operatora trzeba w rzeczywistości zadeklarować odpowiednią funkcję w klasie. W języku C++ wygląda to następująco:

class KLASA  
{ 
public: 
 KLASA operator+(int Value); 
}; 

Powyższy przypadek prezentuje sposób deklaracji operatora dodawania (+). Jest to dość czytelne — używamy słowa kluczowego operator oraz odpowiedniego symbolu.

W Delphi wygląda to nieco inaczej — należy w tym celu zadeklarować funkcję (metodę) o odpowiedniej, z góry ustalonej nazwie — np. Add. W takim przypadku w momencie dodawania do siebie dwóch klas w rzeczywistości zostanie wywołana funkcja Add. Lista operatorów wraz z ich odpowiednikami — nazwami prezentuje tabela 7.1.

Tabela 7.1. Operatory, które można przeładowywać
-Operator | Funkcja odpowiadająca operatorowi
:= | Implicit(a : type) : resultType;
Inc | Inc(a: type) : resultType;
Dec | Dec(a: type): resultType
not | LogicalNot(a: type): resultType;
Trunc | Trunc(a: type): resultType;
Round | Round(a: type): resultType;
= | Equal(a: type; b: type) : Boolean;
<> | NotEqual(a: type; b: type): Boolean;
> | GreaterThan(a: type; b: type) Boolean;
>= | GreaterThanOrEqual(a: type; b: type): resultType;
< | LessThan(a: type; b: type): resultType;
<= | LessThanOrEqual(a: type; b: type): resultType;
+ | Add(a: type; b: type): resultType;
- | Subtract(a: type; b: type) : resultType;
* | Multiply(a: type; b: type) : resultType;
/ | Divide(a: type; b: type) : resultType;
div | IntDivide(a: type; b: type): resultType;
mod | Modulus(a: type; b: type): resultType;
shl | ShiftLeft(a: type; b: type): resultType;
shr | ShiftRight(a: type; b: type): resultType;
and | LogicalAnd(a: type; b: type): resultType;
or | LogicalOr(a: type; b: type): resultType;
xor | LogicalXor(a: type; b: type): resultType;
and | BitwiseAnd(a: type; b: type): resultType;
or | BitwiseOr(a: type; b: type): resultType;
xor | BitwiseXor(a: type; b: type): resultType;

Funkcje Trunc oraz Round służą w Delphi do zaokrąglania liczb rzeczywistych w górę lub w dół.

Deklaracja operatorów

Podsumujmy: aby można było użyć przeładowania operatora, np. :=, należy zadeklarować w klasie odpowiednią funkcję (w tym przypadku — Implicit). Konstrukcja ta jest dość specyficzna:

  TOverload = class
    class operator Implicit(Value : Integer) : TOverload;
  end;

Kluczową rolę odgrywa tutaj słowo kluczowe operator. Należy zwrócić uwagę, że chociaż jest to funkcja, to jednak brak słowa kluczowego — function. Należy za to użyć konstrukcji class operator. Dodatkowo taka „funkcja” musi zwracać zawsze typ odpowiadający nazwie klasy. Czyli w klasie TOverload operator musi zwracać typ TOverload.

Zadeklarowałem właśnie funkcję Implicit, która jest przeładowanym operatorem. Od tej pory można w kodzie użyć następującej frazy:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
var
  X : TOverload;
begin
  X := 10;
end;

{ TOverload }

class operator TOverload.Implicit(Value: Integer): TOverload;
begin
 // kod
end;

W normalnych warunkach kompilator nie pozwoliłby na skompilowanie kodu, w którym do obiektu typu TOverload próbujemy przypisać wartość liczbową (Integer). Jednak dzięki takiej konstrukcji w rzeczywistości wartość liczbowa (liczba 10) zostanie przekazana jako parametr do funkcji Implicit.

Naturalnie parametrem funkcji Implicit może być wartość innego typu — np. String, Real itp. Wszystko zależy od aktualnych potrzeb.

Binary i Unary

Przeładowane operatory mogą kwalifikować się do dwóch kategorii: binary oraz unary. Różnica leży w liczbie elementów (parametrów) owej funkcji. Przykładowo, funkcja Implicit należy do kategorii unary, gdyż ma jeden parametr. Funkcja Add z kolei musi otrzymać dwa parametry i należy do kategorii binary.

Wyjątki

Żaden program nie jest pozbawiony błędów — jest to zupełnie naturalne, gdyż nawet największe firmy, zatrudniające wielu programistów nie są w stanie zlikwidować w swoich produktach wszystkich niedociągnięć (dotyczy to zwłaszcza dużych projektów). Programując w Delphi, mamy możliwość — przynajmniej do pewnego stopnia — zapanowania nad tymi błędami. Błąd może bowiem wynikać z wykonania pewnej operacji, której my, projektanci, się nie spodziewaliśmy. Może też wystąpić wówczas, gdy użytkownik wykona czynności nieprawidłowe dla programu — np. poda złą wartość itp. W takim przypadku program generuje tzw. wyjątki, czyli komunikaty o błędach. My możemy jedynie odpowiednio zareagować na zaistniały wyjątek, np. poprzez wyświetlenie stosownego komunikatu czy chociażby wykonanie pewnej czynności.

Słowo kluczowe try..except

Objęcie danego kodu „kontrolą błędów” odbywa się poprzez umieszczenie go w bloku try..except. Wygląda to tak:

try
  { instrukcje do wykonania }
except
  { instrukcje do wykonania w razie wystąpienia błędu }
end;

Jeżeli kod znajdujący się po słowie try spowoduje wystąpienie błędu, program automatycznie wykona instrukcje umieszczone po słowie except.

Jeżeli program jest uruchamiany bezpośrednio z Delphi (za pomocą klawisza F9), mechanizm obsługi wyjątków może nie zadziałać. Związane jest to z tym, że Delphi automatycznie kontroluje wykonywanie aplikacji i w razie błędu wyświetla stosowny komunikat (rysunek 7.4) oraz zatrzymuje pracę programu. Żeby temu zapobiec, trzeba wyłączyć odpowiednią opcję. W tym celu należy otworzyć menu Tools => Options, kliknąć zakładkę Debugger Options => Borland Debuggers => Language Exceptions i usunąć zaznaczenie pozycji Notify on language Exception.

7.4.jpg
Rysunek 7.4. Okno wyświetlane przez Delphi w przypadku wystąpienia błędu

Przykład: należy pobrać od użytkownika pewne dane, np. liczbę. Dzięki wyjątkom można sprawdzić, czy wartości podane w polu TextBox (biblioteka WinForms) są wartościami liczbowymi:

procedure TWinForm2.Button1_Click(sender: System.Object; e: System.EventArgs);
begin
  try
    Convert.ToInt32(TextBox1.Text); // próba konwersji
    MessageBox.Show('Konwersja powiodła się!');
  except
    MessageBox.Show('Musisz wpisać liczbę!');
  end;
end;

Na samym początku w bloku try następuje próba konwersji tekstu do liczby (wykorzystanie klasy Convert). Jeżeli wszystko odbędzie się pomyślnie, to okienko informacyjne będzie zawierać odpowiedni tekst. Jeżeli natomiast wartość podana przez użytkownika nie będzie liczbą, zostanie wykonany wyjątek z bloku except.

Słowo kluczowe try..finally

Kolejną instrukcją do obsługi wyjątków są słowa kluczowe try oraz finally. W odróżnieniu od bloku except kod znajdujący się po słowie finally będzie wykonywany zawsze, niezależnie od tego, czy wyjątek wystąpi, czy też nie.

Konstrukcji tej używa się np. w sytuacji, gdy konieczne jest zwolnienie pamięci, a nie można zyskać pewności, czy podczas operacji nie wystąpi żaden błąd.

{ rezerwujemy pamięć }
try
  { operacje mogące stać się źródłem wyjątku }
finally
  { zwolnienie pamięci }
end;

Instrukcje try oraz finally są często używane przez programistów podczas tworzenia nowych klas i zwalniania danych — oto przykład:

MojaKlasa := TMojaKlasa.Create;
try
  { jakieś operacje }
finally
  MojaKlasa.Free;
end;

Dzięki temu niezależnie od tego, czy wystąpi wyjątek czy też nie, pamięć zostanie zwolniona! Z taką konstrukcją można spotkać się bardzo często, przeglądając kody źródłowe innych programistów.

W Delphi dla .NET istnieje pewne ułatwienie, gdyż mechanizm garbage collection zapewnia bezpieczeństwo kodu — po zakończeniu wykorzystania klasy pamięć zostanie zwolniona automatycznie. Nie ulega jednak wątpliwości, że zwalnianie klasy metodą Free jest dobrym zwyczajem i powinien być praktykowany przez projektantów aplikacji.

Zagnieżdżanie wyjątków

Możliwe jest również połączenie bloków try oraz except z blokiem try..finally:

MojaKlasa := TMojaKlasa.Create;
try
  try
    { operacje mogące stać się źródłem wyjątków }
  except
    { komunikat informujący o wystąpieniu błędu }
  end;
finally
  MojaKlasa.Free; // zwolnienie pamięci
end;

W takim przypadku w razie wystąpienia błędu w drugim bloku try najpierw zostanie wykonany kod po słowie except, a dopiero później blok finally.

Słowo kluczowe raise

Słowo kluczowe raise służy do tworzenia klasy wyjątku. Brzmi to trochę niejasno, ale w rzeczywistości tak nie jest. Spójrzmy na poniższy kod:

  if Length(TextBox1.Text) = 0 then
    raise Exception.Create('Wpisz jakiś tekst w polu TextBox!');

W przypadku gdy użytkownik nic nie wpisze w polu TextBox1, zostanie wygenerowany wyjątek. Wyjątki są generowane za pomocą klasy Exception, ale o tym opowiem nieco później. Na razie należy zapamiętać, że słowo raise umożliwia generowanie wyjątków poza blokiem try..except.

Pozostawienie słowa raise samego, jak w poniższym przypadku, spowoduje wyświetlenie domyślnego komunikatu o błędzie:

try
  { jakieś funkcje }
except
  raise;
end;

Jeżeli w tym przypadku w bloku try znajdą się instrukcje, które doprowadzą do wystąpienia błędu, to słowo kluczowe raise spowoduje wyświetlenie domyślnego komunikatu o błędzie dla tego wyjątku.

Nie można jednak używać samego słowa raise poza blokiem try..except — w takim przypadku zostanie wyświetlony komunikat o błędzie: [Error] Unit1.pas(29): Re-raising an exception only allowed in exception handler.

Klasa Exception

W module SysUtils jest zadeklarowana klasa Exception (wyjątkowo bez litery T na początku), która jest klasą bazową dla wszystkich wyjątków. W rzeczywistości działa na tej zasadzie co klasa TObject oraz System.Object. Klasa System.Object jest główną klasą .NET, a TObject, korzystając z mechanizmu class helpers, rozszerza ją o nowe możliwości.

Klasą obsługi wyjątków w .NET jest System.Exception, a w Delphi jej funkcjonalność została rozszerzona z wykorzystaniem mechanizmu class helpers i w ten sposób mamy po prostu klasę Exception.

W Delphi istnieje kilkadziesiąt klas wyjątków (wszystkie dziedziczą po klasie Exception), a każda klasa odpowiada za obsługę innego wyjątku. Przykładowo, wyjątek EConvertError występuje podczas błędów konwersji, a EDivByZero — podczas próby dzielenia liczb przez 0. Wszystko to jest związane z tzw. selektywną obsługą wyjątków, o czym będę mówił za chwilę.

W każdym razie można zadeklarować w programie własny typ wyjątku.

type
  ELowError = class(Exception);
  EMediumError = class(Exception);
  EHighError = class(Exception);

Przyjęło się już, że nazwy wyjątków rozpoczynają się od litery E — Czytelnikowi także zalecam stosowanie takiego nazewnictwa. Od mementu zadeklarowania nowego typu można generować takie wyjątki:

raise EHighError.Create('Coś strasznego! Zakończ aplikację!');

Obiekt EHighError jest zwykłą klasą dziedziczoną po Exception, należy więc także wywołać jej konstruktor. Tekst wpisany w apostrofy zostanie wyświetlony w oknie komunikatu o błędzie (rysunek 7.5).

7.5.jpg
Rysunek 7.5. Komunikat o błędzie wygenerowany za pomocą klasy EHighError

Selektywna obsługa wyjątków

Selektywna obsługa wyjątków polega na wykrywaniu rodzaju błędu i wyświetleniu stosownej informacji (lub wykonaniu jakiejś innej czynności).

try
  { instrukcje mogące spowodować błąd }
except
  on ELowError do { jakiś komunikat }
  on EHighError do { jakiś komunikat }
end;

Właśnie przedstawiłem zastosowanie kolejnego operatora języka Delphi — on. Jak widać, dzięki niemu można określić typ wyjątku i przewidzieć odpowiednią reakcję. Delphi rozróżnia kilkadziesiąt klas wyjątków, jak np. EDivByZero (błąd związany z dzieleniem przez 0), EInvalidCast (związany z nieprawidłowym rzutowaniem) czy EConvertError (związany z nieprawidłowymi operacjami konwertowania liczb oraz tekstu). Więcej można dowiedzieć się z systemu pomocy Delphi.

Zdarzenie OnException

Na próżno szukać zdarzenia OnException na liście zakładki Events inspektora obiektów. Zdarzenie OnException jest związane z całą aplikacją, a nie jedynie z formularzem — stąd znajduje się w klasie TApplication VCL.NET (nie występuje w WinForms)!

Dzięki temu zdarzeniu można przechwycić wszystkie komunikaty o błędach występujące w danej aplikacji. Jest to jednak odmienna forma zdarzenia, której nie generuje się z poziomu inspektora obiektów. Trzeba w programie napisać nową procedurę, która będzie obsługiwała zdarzenie OnException.

Deklaracja takiej procedury musi wyglądać następująco:

    procedure MyAppException(Sender: TObject; E : Exception);

Drugi parametr @@E@@ zawiera wyjątek, który wystąpił w programie. Warto może wyjaśnić, dlaczego deklaracja wygląda właśnie w taki sposób. Kiedy zdarzenia były obsługiwane z poziomu inspektora obiektów — np. OnMouseMove — zawierały one specyficzne parametry dotyczące określonej sytuacji (w przypadku OnMouseMove były to współrzędne wskaźnika myszy oraz parametr Shift). Delphi nie dopuści do uruchomienia programu w przypadku, gdy procedura zdarzeniowa OnException nie będzie zawierała parametru @@E@@.

Aby rzeczywiście móc przechwytywać wyjątki zaistniałe w programie, należy wykonać jeszcze jedną czynność:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Application.OnException := MyAppException;
end;

W efekcie program będzie obsługiwał wszelkie zaistniałe wyjątki za pomocą procedury MyAppException.

Obsługa wyjątków

Mamy już procedurę, która będzie obsługiwała zdarzenie OnException, ale to jeszcze nie wszystko. Trzeba jeszcze procedurę MyAppException jakoś oprogramować i określić, jakie czynności będą wykonywane w przypadku wystąpienia wyjątków.

procedure TMainForm.MyAppException(Sender: TObject; E: Exception);
begin
{ wyświetlenie komunikatów wyjątków }
  Application.ShowException(E);
  
  if E is EHighError then // jeżeli wyjątek to EHighError...
  begin
    if Application.MessageBox('Dalsze działanie programu grozi zawieszeniem systemu. Czy chcesz kontynuować?',
    'Błąd', MB_YESNO + MB_ICONWARNING) = Id_No then Application.Terminate;
  end;
end;

Pierwszy wiersz powyższej procedury stanowi wykonanie polecenia ShowException z klasy Application. Polecenie to powoduje wyświetlenie komunikatu związanego z danym wyjątkiem (rysunek 7.5).

Kolejne instrukcje stanowią już tylko przykład tego, jak można zareagować w sytuacji wystąpienia jakiegoś konkretnego błędu (listing 7.2).

Listing 7.2. Kod modułu MainForm

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ComCtrls;

type
  TMainForm = class(TForm)
    rgExceptions: TRadioGroup;
    btnGenerate: TButton;
    StatusBar: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure btnGenerateClick(Sender: TObject);
  private
    procedure MyAppException(Sender: TObject; E : Exception);
  public
    { Public declarations }
  end;

  ELowError = class(Exception);
  EMediumError = class(Exception);
  EHighError = class(Exception);

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

{ TMainForm }

procedure TMainForm.MyAppException(Sender: TObject; E: Exception);
begin
{ wyświetlenie komunikatów wyjątków }
  Application.ShowException(E);
  
  if E is EHighError then // jeżeli wyjątek to EHighError...
  begin
    if Application.MessageBox('Dalsze działanie programu grozi zawieszeniem systemu. Czy chcesz kontynuować?',
    'Błąd', MB_YESNO + MB_ICONWARNING) = Id_No then Application.Terminate;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ przypisanie zdarzeniu OnException procedury MyAppException }
  Application.OnException := MyAppException;
end;

procedure TMainForm.btnGenerateClick(Sender: TObject);
begin
{ odczytanie pozycji z komponentu TRadioGroup }
  case rgExceptions.ItemIndex of
    0: raise ELowError.Create('Niegroźny błąd!');
    1: raise EMediumError.Create('Niebezpieczny błąd!');
    2: raise EHighError.Create('Bardzo niebezpieczny błąd!');
  end;
end;

end.

Zamiast standardowego wyświetlenia opisu błędu w komunikacie informacyjnym (co w listingu 7.2 jest efektem wykonania polecenia ShowException) jest możliwe wyświetlenie komunikatu, np. w komponencie aplikacji. Wystarczy zmodyfikować kod z listingu 7.2 i w zdarzeniu MyAppException napisać:

  StatusBar.SimpleText := E.Message;

7.6.jpg
Rysunek 7.6. Program podczas działania

Należy zaznaczyć, że powyższy przykładowy program jest napisany dla biblioteki VCL.NET.

Identyfikatory

Identyfikator jest nowym elementem wprowadzonym w Delphi 8. Ze strony Delphi jest to symbol &, który nie może zostać wykorzystany samodzielnie — nie jest więc operatorem Delphi lecz kluczowym znakiem CLR. Najczęstszym zastosowaniem tego identyfikatora jest poprzedzenie słowa kluczowego zarezerwowanego dla danego języka — np.:

var
  MyType : System.Type;

Powyższy kod zostanie wykonany prawidłowo — zadeklarowano zmienną @@MyType@@ wskazującą na klasę System.Type. Istnieje możliwość usunięcia pierwszego członu i pozostawienia samej nazwy klasy:

var
  MyType : Type;

Taki zapis nie zostanie przyjęty przez kompilator, gdyż słowo kluczowe type stosuje się w celu nadania nowego typu. Należy wówczas poprzedzić słowo type znakiem &:

MyType : &Type;

Boksowanie typów

W .NET Framework, w wielu przypadkach metody wymagają podania parametru typu System.Object. Przykładowo, utwórzmy nowy projekt WinForms i umieśćmy na formularzu komponent ListBox, a także przycisk Button. Zadaniem programu będzie umieszczanie elementów na liście ListBox po naciśnięciu przycisku. Klasa ListBox posiada właściwość @@Items@@, która służy do zarządzania elementami na liście. Nowy element można dodać wywołując metodę Add, w następujący sposób:

  ListBox1.Items.Add();

Oczywiście w nawiasie należy podać wartość, która zostanie dodana do listy. Metoda Add posiada parametr, który jest typu System.Object. Aby dodać wartość liczbową, należy skorzystać z mechanizmu boksowania:

procedure TWinForm2.Button2_Click(sender: System.Object; e: System.EventArgs);
begin
  ListBox1.Items.Add(System.&Object('Delphi jest fajne'));
end;

Po uruchomieniu programu i naciśnięciu przycisku, do listy zostanie dodany nowy element.
Teraz powinienem wyjaśnić, iż boksowanie jest techniką pozwalającą na konwersję pewnych danych na typ System.Object.

.NET Framework umożliwia także operację odwrotną — warto przeanalizować poniższy przykład:

procedure TWinForm2.Button2_Click(sender: System.Object; e: System.EventArgs);
type
  TPoint = packed record
    X, Y : Integer;
  end;

var
  P : TPoint;
  O : System.Object;
begin
  P.X := 1;
  P.Y := 2;

  { pakowanie (boksowanie) }
  O := System.Object(P);

  { odpakowywanie }
  P := TPoint(O);

  MessageBox.Show('Wartość X: ' + Convert.ToString(P.X));
end;

Zadeklarowałem rekord o nazwie TPoint oraz zmienną @@P@@, wskazującą na ten rekord. Po przydzieleniu danych do tego rekordu, zastosowałem boksowanie, aby dokonać konwersji danych do typu System.Object. Później zastosowałem operację odwrotną — rzutowanie na typ TPoint. Praktyczne wykorzystanie mechanizmu boksowania opisałem w dalszej części rozdziału.

Przykład wykorzystania klas

Wydaje mi się, że już dość powiedziałem o klasach, nie prezentując żadnego konkretnego przykładu. Poświęćmy więc trochę czasu na napisanie aplikacji, która będzie wykorzystywała klasy. Niech to będzie popularna gra, Kółko i krzyżyk. Na samym początku napiszemy „silnik” aplikacji, który będzie zawarty w osobnym module, w klasie, nazwijmy ją — TGomoku (niekiedy gra Kółko i krzyżyk jest właśnie tak nazywana). Aby lepiej zaprezentować pewną cechę klas, najpierw napiszemy interfejs konsolowy, który będzie wykorzystywał nasz silnik, a dopiero później skorzystamy z WinForms.

Zasady gry

Wydaje mi się, że większość Czytelników zna zasady gry Kółko i krzyżyk, lecz na wszelki wypadek je przypomnę. W najpopularniejszym wydaniu gra odbywa się na planszy 3x3. Gracze na przemian umieszczają w polach swoje znaki, dążąc do zajęcia trzech pól w jednej linii. Gracz ma przyporządkowany znak krzyżyka (X) lub kółka (O). Jedno pole może być zajęte przez jednego gracza i nie zmienia się przez cały przebieg gry (rysunek 7.7).

7.7.jpg
Rysunek 7.7. Prezentacja gry Kółko i krzyżyk

Specyfikacja klasy

Zacznijmy od początku. Plansza do gry Kółko i krzyżyk składa się w sumie z dziewięciu pól, po trzy w pionie i w poziomie. Będzie więc potrzebna tablica dwuwymiarowa 2x3:

TField = array[1..3, 1..3] of TFieldType;

Typ TFieldType to wartość liczbowa, którą zadeklarowałem na potrzeby naszego programu:

  TFieldType = ftCircle..ftCross;

Natomiast ftCircle oraz ftCross to stałe:

const
  ftCircle = 1;
  ftCross = 10;

Określają one krzyżyk (ftCross) oraz kółko (ftCircle). Mamy więc typ TFieldType, który może przybrać wartości od 1 do 10 oraz dwuwymiarową tablicę liczbową, która ma odwzorowywać planszę do gry. Aby umożliwić użytkownikowi umieszczenie krzyżyka lub kółka w danym polu, trzeba odwołać się do konkretnego elementu tablicy:

Field[1,1] := ftCross;

Powyższy kod spowoduje umieszczenie krzyżyka w lewym górnym rogu.

Teraz utwórzmy nowy projekt aplikacji konsolowej .NET, a następnie — nowy moduł. Ja nazwałem go GomokuUnit.pas, a samą aplikację — GomokuApp. W module umieścimy silnik naszej aplikacji, czyli klasę TGomoku. Teraz warto zastanowić się, jakie zapisy powinny w tej klasie znaleźć się w sekcji publicznej, aby gotowa gra spełniała oczekiwania użytkowników. A jakie są oczekiwania? Funkcjonalność gry nie musi być duża, wystarczy funkcja, która na podstawie współrzędnych X i Y umieści w tablicy odpowiednie pole (krzyżyk lub kółko). Trzeba oczywiście zapewnić dostęp do tej tablicy, czyli klasę tę należy zadeklarować w sekcji public. Dodatkowo przydałyby się właściwości, dzięki którym będzie można określić imiona graczy. Co poza tym? Na pewno będziemy chcieli wiedzieć, czy gra została zakończona i kto wygrał. Potrzebne będą jeszcze dwie metody — Start oraz NewGame. Pierwsza rozpoczyna grę, druga resetuje ustawienia (liczbę zwycięstw poszczególnych graczy).

Ustawienia gracza

Gracze będą identyfikowani za pomocą ich nazw. Na samym początku gry użytkownicy będą mogli wpisać swoje imiona. Proponuję więc pójść dalej i utworzyć nowy rekord — TPlayer, który będzie zawierał informacje o graczu:

  { rekord odzwierciedlający gracza }
  TPlayer = record
    Name : String[30]; // nazwa gracza
    &Type : TFieldType; // czy gracz używa kółka czy krzyżyka?
    Winnings : Byte; // ilość wygranych w danej partii
  end;

W rekordzie znajduje się nazwa użytkownika, symbol, jakim się posługuje (pole @@Type@@), czyli kółko (ftCircle) lub krzyżyk (ftCross) oraz liczba zwycięstw (@@Winnings@@). Skoro graczy będzie dwóch, to można w sekcji strict private klasy utworzyć tablicę dwuelementową:

    FPlayer : array[ptPlayer1..ptPlayer2] of TPlayer;

Obsługa wyjątków

Na samym początku napiszemy aplikację konsolową, która będzie obsługiwała naszą klasę. Aby umieścić symbol w danym polu, użytkownik będzie musiał wpisać jego współrzędne. W tym momencie należy wprowadzić mechanizm walidacji danych, czyli sprawdzenia poprawności wpisanych wartości. Użytkownik może się zagalopować i podać współrzędne pola, które zostało już zajęte przez drugiego gracza. Może też podać nieprawidłowe wartości. Umieścimy więc w naszym module dwa wyjątki:

  { wyjątek - zła wartość przekraczająca współrzędne }
  EBadValue = class(Exception);
  { wyjątek - dane pole jest już zajęte }
  EFieldNotEmpty = class(Exception);

Pierwszy z nich będzie wywoływany w momencie podania nieprawidłowej liczby, a drugi — gdy wybrane pole zostało już wcześniej zajęte.

Zarys klasy

Zgodnie z tym co powiedziałem, można utworzyć pewien zarys klasy. Przyjrzyjmy się poniższemu fragmentowi kodu:

unit GomokuUnit;

interface

const
  { stałe określające kółko oraz krzyżyk }
  ftCircle = 1;
  ftCross = 10;

  { stałe określające graczy }
  ptPlayer1 = 1;
  ptPlayer2 = 2;

type
  { nowy typ danych }
  TFieldType = ftCircle..ftCross;
  { tablica odzwierciedla plansze do gry }
  TField = array[1..3, 1..3] of TFieldType;

  { rekord reprezentujący gracza }
  TPlayer = record
    Name : String[30]; // nazwa gracza
    &Type : TFieldType; // czy gracz używa kółka czy krzyżyka?
    Winnings : Byte; // liczba wygranych w danej partii
  end;

  { liczba wygranych gracza }
  TWinnigs = array[ptPlayer1..ptPlayer2] of Byte;

  { wyjątek - zła wartość przekraczająca współrzędne }
  EBadValue = class(Exception);
  { wyjątek - dane pole jest już zajęte }
  EFieldNotEmpty = class(Exception);

  TGomoku = class
  strict private
    FWinner : Boolean;
    FField : TField;
    FPlayer : array[ptPlayer1..ptPlayer2] of TPlayer;
    FActive : Byte;
    procedure Sum(Value : Integer);
    procedure CheckWinner;
    function GetPlayer1 : String;
    procedure SetPlayer1(const Value: String);
    function GetPlayer2: String;
    procedure SetPlayer2(const Value: String);
    function GetActive: TPlayer;
    function GetWinnigs: TWinnigs;
  public
    { właściwość reprezentuje aktualnego gracza }
    property Active : TPlayer read GetActive;
    { właściwość określa imię pierwszego gracza }
    property Player1 : String read GetPlayer1 write SetPlayer1;
    { właściwość określa imię drugiego gracza }
    property Player2 : String read GetPlayer2 write SetPlayer2;
    { właściwość tylko do odczytu, reprezentuje planszę do gry }
    property Field : TField read FField;
    { właściwość określa, czy gra została zakończona }
    property Winner : Boolean read FWinner;
    { liczba wygranych gracza }
    property Winnings : TWinnigs read GetWinnigs;
    { metoda: rozpoczęcie gry }
    procedure Start;
    { metoda: nowa gra }
    procedure NewGame;
    { ustawienie symbolu w danym polu }
    procedure &Set(X, Y : Integer);
    { konstruktor klasy }
    constructor Create; 
  end;

W klasie nie ma pól w sekcji public — zadeklarowano same właściwości oraz metody. Dodatkowo większość pól jest tylko do odczytu — wartości można przypisać jedynie dwóm polom: @@Player1@@ oraz @@Player2@@ (imiona graczy).

Zacznijmy od rzeczy najważniejszej, czyli od pola @@FPlayer@@ w sekcji strict private. Jest to dwuelementowa tablica typu TPlayer, przechowująca informacje dotyczące graczy. Pole @@FActive@@, typu Byte, przechowuje informację, który użytkownik aktualnie dokonuje ruchu.

Dla użytkownika naszej klasy zapewne ważna będzie właściwość @@Active@@, która reprezentuje danego gracza. Ów właściwość typu TPlayer jest tylko do odczytu, dane zwracane przez tę właściwość są odczytywane za pomocą funkcji GetActive:

function TGomoku.GetActive: TPlayer;
begin
  Result := FPlayer[FActive];
end;

Informacje o użytkowniku są zwracane na podstawie pola @@FActive@@.

Najważniejsze funkcje klasy to m.in. procedura Set, która na podstawie współrzędnych X i Y umieszcza odpowiedni symbol w tablicy Field. Druga ważna metoda klasy to CheckWinner, która po każdym ruchu użytkownika sprawdza, czy grę można zakończyć. W tym celu należy opracować odpowiedni algorytm, który jest najtrudniejszą częścią programu, ale tym zajmiemy się później.

Najpierw przyjrzyjmy się metodzie Set:

procedure TGomoku.&Set(X, Y: Integer);
begin
  { sprawdzenie, czy pole jest puste }
  if Field[X, Y] > 0 then
  begin
    raise EFieldNotEmpty.Create;
  end;

  { sprawdzenie, czy podano prawidłowe współrzędne }
  if (X > 3) or (Y > 3) then
  begin
    raise EBadValue.Create;
  end;

  { przydzielenie figury do pola }
  FField[X, Y] := GetActive.&Type;

  { sprawdzenie, czy można zakończyć grę }
  CheckWinner;

  { jeżeli gra nie została zakończona, zmieniamy graczy }
  if not Winner then
  begin
    if FActive = ptPlayer1 then
      FActive := ptPlayer2
    else FActive := ptPlayer1;
  end;
end;

Na samym początku program przeprowadza proces walidacji danych. Sprawdza, czy dane pole nie jest zajęte, a jeśli jest — generuje wyjątek. Następnie trzeba zweryfikować poprawność współrzędnych (wartość X i Y nie może przekraczać 3).

Po prawidłowo przeprowadzonym procesie walidacji należy przydzielić znak do określonego pola w polu @@FField@@. Kolejnym krokiem jest wywołanie metody CheckWinner, która sprawdza, czy można już zakończyć grę.

Na samym końcu, po zakończeniu tury, zmieniamy graczy.

Sprawdzenie wygranej

Metoda CheckWinner ma sprawdzać, czy któryś z graczy wygrał, tj. czy zapełnił trzy pola w jednej linii. Zastanówmy się, ile może być kombinacji wygranych w grze Kółko i krzyżyk? Skoro pól jest dziewięć, można naliczyć osiem wygranych kombinacji (trzy w poziomie, trzy w pionie i dwa na ukos). Spójrzmy na rysunek 7.8.

7.8.jpg
Rysunek 7.8. Plansza do gry Kółko i krzyżyk

Wyobraźmy sobie, że plansza na rysunku 7.8 odwzorowuje tablicę @@FFields@@ w klasie TGomoku. Warto zauważyć, że pole @@FFields@@ wskazuje na typ TField, który jest typu tablicowego, a elementy tablicy są w rzeczywistości liczbami. Kółko odpowiada cyfrze 1, a krzyżyk liczbie 10, tak jak to zostało przedstawione na rysunku 7.8. Jak więc sprawdzić, czy gracz zakończył grę zwycięstwem? Wystarczy zsumować wartości pól w jednej linii. Przykładowo: na planszy znalazły się trzy znaki krzyżyka, ustawione obok siebie w linii poziomej (tak jak to zaprezentowano na rysunku 7.8). Po zsumowaniu wartości tych elementów tablicy otrzymamy liczbę 30. Gdyby zamiast krzyżyków umieścić kółka, otrzymamy cyfrę 3. Oto dwie metody, które odpowiadają za sprawdzenie zwycięscy gry:

{ funkcja sprawdza, czy można zakończyć grę }
procedure TGomoku.CheckWinner;
var
  I : Integer;
begin
  for I := 1 to 3 do
  begin
    Sum(FField[I, 1] + FField[I, 2] + FField[I, 3]);
    Sum(FField[1, I] + FField[2, I] + FField[3, I]);
  end;

  Sum(FField[1, 1] + FField[2, 2] + FField[3, 3]);
  Sum(FField[1, 3] + FField[2, 2] + FField[3, 1]);
end;

procedure TGomoku.Sum(Value: Integer);
begin
{ jeżeli wartość to 3 lub 30 - koniec gry, ktoś wygrał }
  if (Value = (3 * ftCircle)) or (Value = (3 * ftCross)) then
  begin
    { zwiększenie liczby wygranych aktualnemu użytkownikowi }
    Inc(Fplayer[FActive].Winnings);
    { zmiana pola - koniec gry } 
    FWinner := True;
  end;
end;

Metoda CheckWinner sprawdza wszystkie kombinacje, sumując po trzy pola z tablicy i przekazując zsumowaną wartość do metody Sum. Metoda Sum sprawdza, czy przekazana wartość (parametr @@Value@@) równa się liczbie 3 lub 30. Jeżeli tak jest, można zakończyć grę i ogłosić zwycięzcę (wartość pola @@FWinner@@ zmieniamy na True).

Pełny kod źródłowy modułu GomokuUnit znajduje się na listingu 7.3.

Listing 7.3. Kod źródłowy modułu GomokuUnit

unit GomokuUnit;

interface

const
  { stałe określające kółko oraz krzyżyk }
  ftCircle = 1;
  ftCross = 10;

  { stałe określające graczy }
  ptPlayer1 = 1;
  ptPlayer2 = 2;

type
  { nowy typ danych }
  TFieldType = ftCircle..ftCross;
  { tablica odzwierciedla planszę do gry }
  TField = array[1..3, 1..3] of TFieldType;

  { rekord reprezentujący gracza }
  TPlayer = record
    Name : String[30]; // nazwa gracza
    &Type : TFieldType; // czy gracz używa kółka czy krzyżyka?
    Winnings : Byte; // liczba wygranych w danej partii
  end;

  { liczba wygranych gracza }
  TWinnigs = array[ptPlayer1..ptPlayer2] of Byte;

  { wyjątek - zła wartość przekraczająca współrzędne }
  EBadValue = class(Exception);
  { wyjątek - dane pole jest już zajęte }
  EFieldNotEmpty = class(Exception);

  TGomoku = class
  strict private
    FWinner : Boolean;
    FField : TField;
    FPlayer : array[ptPlayer1..ptPlayer2] of TPlayer;
    FActive : Byte;
    procedure Sum(Value : Integer);
    procedure CheckWinner;
    function GetPlayer1 : String;
    procedure SetPlayer1(const Value: String);
    function GetPlayer2: String;
    procedure SetPlayer2(const Value: String);
    function GetActive: TPlayer;
    function GetWinnigs: TWinnigs;
  public
    { właściwość reprezentuje aktualnego gracza }
    property Active : TPlayer read GetActive;
    { właściwość określa imię pierwszego gracza }
    property Player1 : String read GetPlayer1 write SetPlayer1;
    { właściwość określa imię drugiego gracza }
    property Player2 : String read GetPlayer2 write SetPlayer2;
    { właściwość tylko do odczytu, reprezentuje planszę do gry }
    property Field : TField read FField;
    { właściwość określa, czy gra została zakończona }
    property Winner : Boolean read FWinner;
    { liczba wygranych gracza }
    property Winnings : TWinnigs read GetWinnigs;
    { metoda: rozpoczęcie gry }
    procedure Start;
    { metoda: nowa gra }
    procedure NewGame;
    { ustawienie symbolu w danym polu }
    procedure &Set(X, Y : Integer);
    { konstruktor klasy }
    constructor Create; 
  end;

implementation

{ TGomoku }

procedure TGomoku.&Set(X, Y: Integer);
begin
  { sprawdzenie, czy pole jest puste }
  if Field[X, Y] > 0 then
  begin
    raise EFieldNotEmpty.Create;
  end;

  { sprawdzenie, czy podano prawidłowe współrzędne }
  if (X > 3) or (Y > 3) then
  begin
    raise EBadValue.Create;
  end;

  { przydzielenie figury do pola }
  FField[X, Y] := GetActive.&Type;

  { sprawdzenie, czy można zakończyć grę }
  CheckWinner;

  { jeżeli gra nie została zakończona, zmieniamy graczy }
  if not Winner then
  begin
    if FActive = ptPlayer1 then
      FActive := ptPlayer2
    else FActive := ptPlayer1;
  end;
end;

{ metoda zeruje liczby wygranych gracza }
procedure TGomoku.NewGame;
begin
  FPlayer[ptPlayer1].Winnings := 0;
  FPlayer[ptPlayer2].Winnings := 0;
end;

{ metoda rozpoczyna gre - przydziela symbol konkretnemu graczowi }
procedure TGomoku.Start;
begin
  FPlayer[ptPlayer1].&Type := ftCross;
  FPlayer[ptPlayer2].&Type := ftCircle;

  FWinner := False;

  { czyszczenie tablicy }
  System.Array.Clear(Field, 1, High(Field));
end;

{ funkcja sprawdza, czy można zakończyć grę }
procedure TGomoku.CheckWinner;
var
  I : Integer;
begin
  for I := 1 to 3 do
  begin
    Sum(FField[I, 1] + FField[I, 2] + FField[I, 3]);
    Sum(FField[1, I] + FField[2, I] + FField[3, I]);
  end;

  Sum(FField[1, 1] + FField[2, 2] + FField[3, 3]);
  Sum(FField[1, 3] + FField[2, 2] + FField[3, 1]);
end;

procedure TGomoku.Sum(Value: Integer);
begin
{ jeżeli wartość to 3 lub 30 - koniec gry, ktoś wygrał }
  if (Value = (3 * ftCircle)) or (Value = (3 * ftCross)) then
  begin
    { zwiększenie liczby wygranych aktualnemu użytkownikowi }
    Inc(Fplayer[FActive].Winnings);
    { zmiana pola - koniec gry } 
    FWinner := True;
  end;
end;

function TGomoku.GetActive: TPlayer;
begin
  Result := FPlayer[FActive];
end;

constructor TGomoku.Create;
begin
  inherited;

  { ustalenie aktywnego gracza }
  FActive := ptPlayer1;
end;

function TGomoku.GetPlayer1: String;
begin
  Result := FPlayer[ptPlayer1].Name;
end;

function TGomoku.GetPlayer2: String;
begin
  Result := FPlayer[ptPlayer2].Name;
end;

procedure TGomoku.SetPlayer1(const Value: String);
begin
  FPlayer[ptPlayer1].Name := Value;
end;

procedure TGomoku.SetPlayer2(const Value: String);
begin
  FPlayer[ptPlayer2].Name := Value;
end;

function TGomoku.GetWinnigs: TWinnigs;
begin
  Result[ptPlayer1] := FPlayer[ptPlayer1].Winnings;
  Result[ptPlayer2] := FPlayer[ptPlayer2].Winnings;
end;

end.

Interfejs aplikacji

Na samym początku wykorzystamy utworzoną klasę w aplikacji konsolowej. Aby użytkownik mógł zapełnić określone pole, musi podać współrzędną X i Y owego pola. Następnie aplikacja wyświetla aktualny stan gry (rysunek 7.9).

7.9.jpg
Rysunek 7.9. Gra Kółko i krzyżyk w trybie konsoli

Nasz program jest dosyć prosty. Musi działać w pętli, za każdym razem żądając od użytkownika podania współrzędnych. Warunkiem zakończenia pętli jest stwierdzenie zwycięstwa któregoś z graczy:

while (Gomoku.Winner = False) do

Należy jednak pomyśleć o remisie. Taki przypadek także może się zdarzyć, należy więc przerwać działanie pętli, jeżeli liczba ruchów osiągnie wartość 9 (co znaczy, że wszystkie pola będą zapełnione). Do liczenia liczby wykonanych ruchów posłuży zmienna @@Counter@@. Kod aplikacji, pliku źródłowego *.dpr, znajduje się na listingu 7.4.

Listing 7.4. Kod źródłowy gry Kółko i krzyżyk

program GomokuApp;

{$APPTYPE CONSOLE}


uses
  GomokuUnit in 'GomokuUnit.pas';

var
  Gomoku : TGomoku;
  X, Y : Integer;
  I, J : Integer;
  C : Char;
  Counter : Integer;
begin
  Gomoku := TGomoku.Create;
  try
    Console.WriteLine('Kółko i krzyżyk');
    Console.WriteLine('---------------');
    Console.Write('Podaj imie pierwszego gracza: ');
    Gomoku.Player1 := Console.ReadLine;

    Console.Write('Podaj imię drugiego gracza: ');
    Gomoku.Player2 := Console.ReadLine;

    Gomoku.Start;

    Counter := 1;

    Console.WriteLine;

    while (Gomoku.Winner = False) do
    begin
      Console.WriteLine('Tura nr. ' + Convert.ToString(Counter) + ', Gracz: ' + Gomoku.Active.Name);
      Console.Write('Podaj współrzędną X: ');
      X := Convert.ToInt32(Console.ReadLine);
      Console.Write('Podaj współrzędną Y: ');
      Y := Convert.ToInt32(Console.ReadLine);

      try
        Gomoku.&Set(X, Y);

        { zwiększenie licznika tur }
        Inc(Counter);

        { jeżeli do tej pory nikt nie wygrał, to znaczy, że jest remis }
        if Counter = 9 then
          Break;
      except
        on EBadValue do
          Console.WriteLine('Zła wartość! Podaj liczbę z zakresu 1-3');
        on EFieldNotEmpty do
          Console.WriteLine('To pole jest już zajęte!');
      end;

      { rysowanie planszy }
      for I := 1 to 3 do
      begin
        for J := 1 to 3 do
        begin
          { sprawdzenie znaku w danym elemencie tablicy }
          case Gomoku.Field[I, J] of
            ftCross : C := 'X';
            ftCircle: C := 'O';
            else C := '_';
          end;

          Console.Write(' ' + C + ' |');
        end;
        Console.WriteLine;
      end;

      Console.WriteLine;

    end;

    if Gomoku.Winner then
      Console.WriteLine('Gratulacje! Wygrał ' + Gomoku.Active.Name)
    else Console.WriteLine('Remis!');

  finally
    Gomoku.Free;
  end;
  Console.ReadLine;
end.

Właściwie kod z listingu 7.4 nie zawiera elementów, które nie były do tej pory omawiane. Na samym początku następuje utworzenie klasy oraz określenie imion graczy. Dalej, w pętli żądamy od użytkownika podania współrzędnych X i Y, a także wywołujemy metodę Set z obiektu Gomoku. Należy zwrócić uwagę na obsługę wyjątków, czyli wyświetlanie odpowiednich komunikatów w przypadku podania złych wartości w trakcie gry. Na samym końcu w pętli rysujemy prostą planszę, która odzwierciedla aktualny stan gry.

Warto też zwrócić uwagę, że do konwersji danych z typu String do Integer użyłem metody ToInt32, pochodzącej z klasy Convert. W całym programie nie korzystałem z żadnych zewnętrznych modułów VCL, lecz jedynie z klas .NET Framework.

Ćwiczenie dodatkowe

Pisząc grę Kółko i krzyżyk specjalnie nie zaimplementowałem obsługi zakończenia działania programu przed czasem. Użytkownik, który raz rozpocznie grę, musi ją kontynuować do chwili wygranej lub osiągnięcia remisu. Z drugiej strony, gracz powinien mieć możliwość zakończenia działania programu zamiast podania kolejnej współrzędnej.

Tworzenie interfejsu graficznego

W poprzednim przykładzie pokazałem, jak można utworzyć interfejs konsolowy dla gry Kółko i krzyżyk. Powiedzmy sobie szczerze, że to nie jest zbytnio satysfakcjonujące osiągnięcie. Zbudowaliśmy jednak klasę TGomoku, a więc jeśli jej użyjemy w aplikacji Windows Forms, będzie można grać w trybie graficznym. Utwórzmy więc nowy projekt aplikacji Windows Forms i zapiszmy projekt pod nazwą GomokuApp. W folderze, w którym zapisano projekt, należy również umieścić moduł GomokuUnit.pas. Teraz trzeba dodać do listy uses odwołanie do tego modułu:

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data, GomokuUnit;

Teraz trzeba by się zastanowić, czego będzie potrzebował nasz interfejs? Przede wszystkim dziewięciu pól, na których będą umieszczane symbole krzyżyka lub kółka. Do tego celu można użyć komponentów Button. Trzeba więc umieścić je na formularzu. Można pozostawić domyślne nazwy komponentów (czyli Button1, Button2 itd.).

Trzeba również dodać przyciski Start oraz Nowa gra. Na formularzu umieścimy także dwa komponenty TextBox, w których użytkownik będzie mógł podawać imiona graczy. Moja wersja interfejsu dla opisywanej aplikacji znajduje się na rysunku 7.10.

7.10.jpg
Rysunek 7.10. Interfejs dla gry Kółko i krzyżyk

Jak widać na rysunku 7.10, użyłem dodatkowego komponentu GroupBox, na którym umieściłem wspomniane kontrolki służące do zarządzania grą. Właściwość @@Visible@@ przycisku Nowa gra (nazwałem go btnNewGame) zmieniłem na False, dzięki czemu przycisk nie będzie widoczny zaraz po starcie programu. Ostatnim etapem jest odpowiednie dopasowanie rozmiarów dziewięciu przycisków (tworzących planszę).

Warto zaznaczyć wszystkie przyciski, dzięki czemu będzie można jednocześnie edytować ich właściwości. W moim przypadku nadałem im rozmiary równe 50 pikseli w pionie oraz w poziomie. Ostatnim etapem jest zmiana czcionki używanej przez przyciski. W tym celu trzeba odszukać w inspektorze obiektów właściwość @@Font@@, rozwinąć gałąź i zmienić wartość pola @@Size@@ na 12. Dodatkowo można zmienić wartość właściwości @@Bold@@ na True, dzięki czemu czcionka będzie pogrubiona.

Dobrze, załóżmy, że interfejs mamy już gotowy. Należy teraz przejść do trudniejszej części, czyli kodowania.

Gra "Kółko i krzyżyk"

Zacznijmy od rzeczy najprostszej, czyli od oprogramowania zdarzeń dla przycisków btnNewGame (Nowa gra) oraz btnStart (Start). Jeżeli chodzi o przycisk rozpoczynający nową grę, to nie ma tutaj większej filozofii:

procedure TGomokuForm.edtNewGame_Click(sender: System.Object; e: System.EventArgs);
begin
  { wywołujemy metodę nakazującą rozpoczęcie nowej gry }
  Gomoku.NewGame;
  { wywołujemy procedurę zdarzeniową Click komponentu btnStart }
  btnStart_Click(Sender, E);
end;

Teraz powinniśmy wywołać metodę NewGame z klasy TGomoku. Dodatkowo, w kodzie metody umieściłem instrukcję nakazującą wywołanie procedury zdarzeniowej dla przycisku btnStart:

btnStart_Click(Sender, E);

Po naciśnięciu przycisku Start pola na planszy powinny być czyszczone. Następuje również wywołanie metody Start z klasy TGomoku. Oto kod procedury zdarzeniowej Click dla komponentu btnStart:

procedure TGomokuForm.btnStart_Click(sender: System.Object; e: System.EventArgs);
var
  I : Integer;
begin
  { sprawdzenie, czy użytkownik podał imiona graczy }
  if (edtPlayer1.Text.Length = 0) or (edtPlayer2.Text.Length = 0) then
    MessageBox.Show('Podaj imiona graczy!');

  Gomoku.Player1 := edtPlayer1.Text;
  Gomoku.Player2 := edtPlayer2.Text;

  { rozpoczęcie gry }
  Gomoku.Start;

  { zmiana właściwości określającej rozpoczęcie gry }
  Started := True;

  lblResults.Text := 'Wynik: ' + Convert.ToString(Gomoku.Winnings[ptPlayer1]) + ' - ' + Convert.ToString(Gomoku.Winnings[ptPlayer2]);

  GroupBox1.Text := 'Tura dla: ' + edtPlayer1.Text;

  { wyczyszczenie zawartości przycisków }
  for I := 0 to Controls.Count -1 do
  begin
    if (Controls[i] is Button) then
      (Controls[i] as Button).Text := '';
  end;

  { pokazanie przycisku "Nowa gra" }
  btnNewGame.Visible := True;
end;

Jak widać, kod tej procedury jest nieco dłuższy. Na samym początku trzeba sprawdzić, czy użytkownik podał imiona graczy, tj. sprawdzamy długość tekstu wpisanego w kontrolkach edtPlayer1 oraz edtPlayer2:

if (edtPlayer1.Text.Length = 0) or (edtPlayer2.Text.Length = 0) then
    MessageBox.Show('Podaj imiona graczy!');

Następnie należy przypisać imiona graczy do odpowiednich właściwości klas TGomoku i wywołać metodę Start. W naszej wersji gry, dopóki użytkownik nie naciśnie przycisku Nowa Gra, są liczone zwycięstwa obu graczy. Dlatego na formularzu umieściłem także komponent Label, który nazwałem lblResults. Będzie on prezentował wynik gry.

Należy zwrócić uwagę na pętlę, która znajduje się na samym końcu procedury zdarzeniowej. Po naciśnięciu przycisku Start plansza musi być czyszczona i to właśnie jest zadanie wspomnianej pętli. Jest to doskonały przykład do zaprezentowania właściwości operatorów as oraz is, o których wspomniałem nieco wcześniej. Właściwość @@Controls@@ określa wszystkie komponenty, które są umieszczone na formularzu. Tak więc w pętli następuje sprawdzenie typu danego komponentu: jeżeli jest to komponent Button, można skorzystać z rzutowania (operator as) i zmienić wartość właściwości @@Text@@.

Obsługa właściwości Tag

Każdy komponent VCL.NET, VCL czy WinForms, posiada właściwość @@Tag@@, która umożliwia przechowywanie dodatkowych informacji na potrzeby budowanej aplikacji. Akurat teraz nadarza się okazja, aby wykorzystać możliwości tej właściwości.

W starszych wersjach Delphi właściwość @@Tag@@ była typu Integer, później typ został zmieniony na Variant, dzięki czemu można do niej przypisać nie tylko dane liczbowe. W WinForms właściwość @@Tag@@ jest typu System.Object. Należy jednak pamiętać, że w bibliotece VCL/VCL.NET właściwość @@Tag@@ nadal jest typu Variant.

Każdy z dziewięciu przycisków na planszy będzie korzystał z jednej procedury zdarzeniowej Click. Za pomocą parametru Sender można sprawdzić, z jakiego komponentu pochodzi zdarzenie i ustawić odpowiednią wartość dla właściwości @@Text@@ — np.:

(Sender as Button).Text := Symbol;

Oprócz tego trzeba jednak wywołać metodę Set z klasy TGomoku, podając współrzędne X i Y. Skąd możemy wiedzieć który przycisk został naciśnięty? Przykładowo, dzięki użyciu instrukcji warunkowej:

if (Sender as Button).Name = 'Button1' then
begin
{ określenie współrzędnych }
   X := 1;
   Y := 1;
end
else 
if (Sender as Button).Name = 'Button2' then
begin
{ ... }
end;

Takie rozwiązanie jest jednak mało zaawansowane pod względem programistycznym. Do właściwości @@Tag@@ każdego z przycisków przypiszemy więc rekord, który będzie określał współrzędne przycisku. Np. dla pierwszego przycisku, znajdującego się w lewym górnym rogu współrzędnymi są (1, 1). Biblioteka klas środowiska .NET Framework zawiera klasę Point, która służy właśnie do przechowywania współrzędnych, można jej użyć w następujący sposób:

Button1.Tag := Point.Create(1, 1);  

W bibliotece VCL możemy skorzystać z rekordu TPoint, który jest odpowiednikiem klasy Point z .NET Framework. Budowa tego rekordu jest następująca:

TPoint = packed record
  X, Y : Integer;
end;
</dfn>

Spójrzmy teraz na edytor kodu. W module WinForms znajduje się metoda InitializeComponent, w której są zawarte instrukcje tworzące komponenty na formularzu. W przeważającej większości przypadków nie będzie trzeba ingerować w jej kod, ponieważ właściwości komponentów można ustawiać z poziomu inspektora obiektów. Tym razem nie możemy jednak skorzystać z inspektora obiektów, konieczna będzie zmiana kodu procedury InitializeComponent. Spójrzmy na poniższy fragment kodu:

  //
  // Button1
  //
  Self.Button1.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12, 
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button1.Location := System.Drawing.Point.Create(8, 8);
  Self.Button1.Name := 'Button1';
  Self.Button1.Size := System.Drawing.Size.Create(50, 50);
  Self.Button1.TabIndex := 1;
  Self.Button1.Tag := System.Drawing.Point.Create(1, 1);
  Include(Self.Button1.Click, Self.Button3_Click);

Zadaniem tego fragmentu kodu jest tworzenie przycisku Button1 na formularzu. Warto zwrócić uwagę, że są przypisywane tutaj wszelkie właściwości komponentu, które odpowiadają za jego wygląd oraz położenie. Nas jednak interesuje wiersz, który wyróżniono pogrubioną czcionką. Odpowiada ona za przypisywanie współrzędnych do właściwości @@Tag@@. W taki sam sposób należy przypisać współrzędne do właściwości @@Tag@@, dla pozostałych ośmiu komponentów.

Pełny kod modułu znajduje się na listingu 7.5.

Ostatnim krokiem jest wygenerowanie zdarzenia Click dla jednego z przycisków z planszy. Wszystkie przyciski będą korzystały z tej samej procedury zdarzeniowej:

procedure TGomokuForm.Button3_Click(sender: System.Object; e: System.EventArgs);
var
  Symbol : Char;
  P : System.Drawing.Point;
begin
  { sprawdzenie, czy użytkownik nacisnął przycisk "Start" }
  if not Started then
    MessageBox.Show('Musisz nacisnąć przycisk "Start", aby rozpocząć grę!');

  Symbol := ' ';

  { nadaj wartości właściwościom, w zależności od symbolu użytkownika }
  case Gomoku.Active.&Type of
    ftCross:  Symbol := 'X';
    ftCircle: Symbol := 'O';
  end;

  { rzutowanie wartości właściwości Tag do rekordu P }
  P := Point((Sender as Button).Tag);

  try
    { ustawienie figury na polu }
    Gomoku.&Set(P.X, P.Y);

    { nadanie symbolu dla komponentu }
    (Sender as Button).Text := Symbol;
  except
    on EFieldNotEmpty do
      MessageBox.Show('To pole jest już zajęte!');
  end;

  { sprawdzenie, czy gracz wygrał }
  if Gomoku.Winner then
  begin
    MessageBox.Show('Wygrał gracz ' + Gomoku.Active.Name);

    lblResults.Text := 'Wynik: ' + Convert.ToString(Gomoku.Winnings[ptPlayer1]) + ' - ' + Convert.ToString(Gomoku.Winnings[ptPlayer2]);
  end
  else
  begin
    GroupBox1.Text := 'Tura dla: ' + Gomoku.Active.Name;
  end;
end;

Na samym początku procedury znajduje się warunek sprawdzający, czy gra została rozpoczęta. Nas najbardziej interesuje poniższa linia:

P := Point((Sender as Button).Tag);

W tym miejscu następuje rzutowanie danych umieszczonych we właściwości @@Tag@@ na strukturę Point. Oznacza to, że pola X oraz Y klasy Point zawierają interesujące nas współrzędne. Kod modułu WinForms znajduje się na listingu 7.5, natomiast pełny kod źródłowy umieściłem na płycie CD dołączonej do książki, w katalogu listingi/7/Gomoku Win.

Listing 7.5. Kod źródłowy aplikacji WinForms

unit GomokuFrm;

interface

uses
  System.Drawing, System.Collections, System.ComponentModel,
  System.Windows.Forms, System.Data, GomokuUnit;

type
  TGomokuForm = class(System.Windows.Forms.Form)
  {$REGION 'Designer Managed Code'}
  strict private
    /// <summary>
    /// Required designer variable.
    /// </summary>
    Components: System.ComponentModel.Container;
    GroupBox1: System.Windows.Forms.GroupBox;
    lblPlayer1: System.Windows.Forms.Label;
    lblPlayer2: System.Windows.Forms.Label;
    edtPlayer1: System.Windows.Forms.TextBox;
    edtPlayer2: System.Windows.Forms.TextBox;
    lblResults: System.Windows.Forms.Label;
    btnNewGame: System.Windows.Forms.Button;
    Button2: System.Windows.Forms.Button;
    Button4: System.Windows.Forms.Button;
    Button1: System.Windows.Forms.Button;
    Button7: System.Windows.Forms.Button;
    Button5: System.Windows.Forms.Button;
    Button6: System.Windows.Forms.Button;
    Button3: System.Windows.Forms.Button;
    Button8: System.Windows.Forms.Button;
    Button9: System.Windows.Forms.Button;
    btnStart: System.Windows.Forms.Button;
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    procedure InitializeComponent;
    procedure edtNewGame_Click(sender: System.Object; e: System.EventArgs);
    procedure TGomokuForm_Load(sender: System.Object; e: System.EventArgs);
    procedure Button3_Click(sender: System.Object; e: System.EventArgs);
    procedure btnStart_Click(sender: System.Object; e: System.EventArgs);
  {$ENDREGION}
  strict protected
    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    procedure Dispose(Disposing: Boolean); override;
  private
    Gomoku : TGomoku;
    Started : Boolean;
  public
    constructor Create;
  end;


  [assembly: RuntimeRequiredAttribute(TypeOf(TGomokuForm))]

implementation

{$AUTOBOX ON}

{$REGION 'Windows Form Designer generated code'}
/// <summary>
/// Required method for Designer support -- do not modify
/// the contents of this method with the code editor.
/// </summary>
procedure TGomokuForm.InitializeComponent;
begin
  Self.GroupBox1 := System.Windows.Forms.GroupBox.Create;
  Self.btnStart := System.Windows.Forms.Button.Create;
  Self.btnNewGame := System.Windows.Forms.Button.Create;
  Self.lblResults := System.Windows.Forms.Label.Create;
  Self.edtPlayer2 := System.Windows.Forms.TextBox.Create;
  Self.edtPlayer1 := System.Windows.Forms.TextBox.Create;
  Self.lblPlayer2 := System.Windows.Forms.Label.Create;
  Self.lblPlayer1 := System.Windows.Forms.Label.Create;
  Self.Button9 := System.Windows.Forms.Button.Create;
  Self.Button6 := System.Windows.Forms.Button.Create;
  Self.Button1 := System.Windows.Forms.Button.Create;
  Self.Button7 := System.Windows.Forms.Button.Create;
  Self.Button5 := System.Windows.Forms.Button.Create;
  Self.Button8 := System.Windows.Forms.Button.Create;
  Self.Button3 := System.Windows.Forms.Button.Create;
  Self.Button4 := System.Windows.Forms.Button.Create;
  Self.Button2 := System.Windows.Forms.Button.Create;
  Self.GroupBox1.SuspendLayout;
  Self.SuspendLayout;
  // 
  // GroupBox1
  // 
  Self.GroupBox1.Controls.Add(Self.btnStart);
  Self.GroupBox1.Controls.Add(Self.btnNewGame);
  Self.GroupBox1.Controls.Add(Self.lblResults);
  Self.GroupBox1.Controls.Add(Self.edtPlayer2);
  Self.GroupBox1.Controls.Add(Self.edtPlayer1);
  Self.GroupBox1.Controls.Add(Self.lblPlayer2);
  Self.GroupBox1.Controls.Add(Self.lblPlayer1);
  Self.GroupBox1.Location := System.Drawing.Point.Create(184, 8);
  Self.GroupBox1.Name := 'GroupBox1';
  Self.GroupBox1.Size := System.Drawing.Size.Create(184, 184);
  Self.GroupBox1.TabIndex := 0;
  Self.GroupBox1.TabStop := False;
  Self.GroupBox1.Text := 'Gracze';
  // 
  // btnStart
  // 
  Self.btnStart.Location := System.Drawing.Point.Create(8, 120);
  Self.btnStart.Name := 'btnStart';
  Self.btnStart.Size := System.Drawing.Size.Create(168, 23);
  Self.btnStart.TabIndex := 6;
  Self.btnStart.Text := 'Start';
  Include(Self.btnStart.Click, Self.btnStart_Click);
  // 
  // btnNewGame
  // 
  Self.btnNewGame.Location := System.Drawing.Point.Create(8, 152);
  Self.btnNewGame.Name := 'btnNewGame';
  Self.btnNewGame.Size := System.Drawing.Size.Create(168, 23);
  Self.btnNewGame.TabIndex := 5;
  Self.btnNewGame.Text := 'Nowa gra';
  Self.btnNewGame.Visible := False;
  Include(Self.btnNewGame.Click, Self.edtNewGame_Click);
  // 
  // lblResults
  // 
  Self.lblResults.Location := System.Drawing.Point.Create(8, 80);
  Self.lblResults.Name := 'lblResults';
  Self.lblResults.Size := System.Drawing.Size.Create(168, 23);
  Self.lblResults.TabIndex := 4;
  // 
  // edtPlayer2
  // 
  Self.edtPlayer2.Location := System.Drawing.Point.Create(58, 50);
  Self.edtPlayer2.Name := 'edtPlayer2';
  Self.edtPlayer2.Size := System.Drawing.Size.Create(118, 20);
  Self.edtPlayer2.TabIndex := 3;
  Self.edtPlayer2.Text := '';
  // 
  // edtPlayer1
  // 
  Self.edtPlayer1.Location := System.Drawing.Point.Create(58, 21);
  Self.edtPlayer1.Name := 'edtPlayer1';
  Self.edtPlayer1.Size := System.Drawing.Size.Create(118, 20);
  Self.edtPlayer1.TabIndex := 2;
  Self.edtPlayer1.Text := '';
  // 
  // lblPlayer2
  // 
  Self.lblPlayer2.Location := System.Drawing.Point.Create(7, 53);
  Self.lblPlayer2.Name := 'lblPlayer2';
  Self.lblPlayer2.Size := System.Drawing.Size.Create(53, 23);
  Self.lblPlayer2.TabIndex := 1;
  Self.lblPlayer2.Text := 'Gracz #2:';
  // 
  // lblPlayer1
  // 
  Self.lblPlayer1.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 
      8.25, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 
      (Byte(238)));
  Self.lblPlayer1.ForeColor := System.Drawing.SystemColors.ControlText;
  Self.lblPlayer1.Location := System.Drawing.Point.Create(8, 24);
  Self.lblPlayer1.Name := 'lblPlayer1';
  Self.lblPlayer1.Size := System.Drawing.Size.Create(64, 16);
  Self.lblPlayer1.TabIndex := 0;
  Self.lblPlayer1.Text := 'Gracz #1:';
  //
  // Button9
  //
  Self.Button9.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12, 
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button9.Location := System.Drawing.Point.Create(121, 120);
  Self.Button9.Name := 'Button9';
  Self.Button9.Size := System.Drawing.Size.Create(50, 50);
  Self.Button9.TabIndex := 9;
  Self.Button9.Tag := System.Drawing.Point.Create(3, 3);
  Include(Self.Button9.Click, Self.Button3_Click);
  // 
  // Button6
  // 
  Self.Button6.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button6.Location := System.Drawing.Point.Create(120, 64);
  Self.Button6.Name := 'Button6';
  Self.Button6.Size := System.Drawing.Size.Create(50, 50);
  Self.Button6.TabIndex := 6;
  Self.Button6.Tag := System.Drawing.Point.Create(3, 2);
  Include(Self.Button6.Click, Self.Button3_Click);
  //
  // Button1
  //
  Self.Button1.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button1.Location := System.Drawing.Point.Create(8, 8);
  Self.Button1.Name := 'Button1';
  Self.Button1.Size := System.Drawing.Size.Create(50, 50);
  Self.Button1.TabIndex := 1;
  Self.Button1.Tag := System.Drawing.Point.Create(1, 1);
  Include(Self.Button1.Click, Self.Button3_Click);
  //
  // Button7
  //
  Self.Button7.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button7.Location := System.Drawing.Point.Create(8, 120);
  Self.Button7.Name := 'Button7';
  Self.Button7.Size := System.Drawing.Size.Create(50, 50);
  Self.Button7.TabIndex := 7;
  Self.Button7.Tag := System.Drawing.Point.Create(1, 3);
  Include(Self.Button7.Click, Self.Button3_Click);
  // 
  // Button5
  // 
  Self.Button5.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button5.Location := System.Drawing.Point.Create(64, 64);
  Self.Button5.Name := 'Button5';
  Self.Button5.Size := System.Drawing.Size.Create(50, 50);
  Self.Button5.TabIndex := 5;
  Self.Button5.Tag := System.Drawing.Point.Create(2, 2);
  Include(Self.Button5.Click, Self.Button3_Click);
  //
  // Button8
  //
  Self.Button8.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12, 
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button8.Location := System.Drawing.Point.Create(64, 120);
  Self.Button8.Name := 'Button8';
  Self.Button8.Size := System.Drawing.Size.Create(50, 50);
  Self.Button8.TabIndex := 8;
  Self.Button8.Tag := System.Drawing.Point.Create(2, 3);
  Include(Self.Button8.Click, Self.Button3_Click);
  // 
  // Button3
  // 
  Self.Button3.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button3.Location := System.Drawing.Point.Create(120, 8);
  Self.Button3.Name := 'Button3';
  Self.Button3.Size := System.Drawing.Size.Create(50, 50);
  Self.Button3.TabIndex := 3;
  Self.Button3.Tag := System.Drawing.Point.Create(3, 1);
  Include(Self.Button3.Click, Self.Button3_Click);
  // 
  // Button4
  // 
  Self.Button4.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12,
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button4.Location := System.Drawing.Point.Create(8, 64);
  Self.Button4.Name := 'Button4';
  Self.Button4.Size := System.Drawing.Size.Create(50, 50);
  Self.Button4.TabIndex := 4;
  Self.Button4.Tag := System.Drawing.Point.Create(1, 2);
  Include(Self.Button4.Click, Self.Button3_Click);
  //
  // Button2
  //
  Self.Button2.Font := System.Drawing.Font.Create('Microsoft Sans Serif', 12, 
      System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (Byte(238)));
  Self.Button2.Location := System.Drawing.Point.Create(64, 8);
  Self.Button2.Name := 'Button2';
  Self.Button2.Size := System.Drawing.Size.Create(50, 50);
  Self.Button2.TabIndex := 2;
  Self.Button2.Tag := System.Drawing.Point.Create(2, 1);
  Include(Self.Button2.Click, Self.Button3_Click);
  // 
  // TGomokuForm
  // 
  Self.AutoScaleBaseSize := System.Drawing.Size.Create(5, 13);
  Self.ClientSize := System.Drawing.Size.Create(392, 213);
  Self.Controls.Add(Self.Button2);
  Self.Controls.Add(Self.Button4);
  Self.Controls.Add(Self.Button3);
  Self.Controls.Add(Self.Button8);
  Self.Controls.Add(Self.Button5);
  Self.Controls.Add(Self.Button7);
  Self.Controls.Add(Self.Button1);
  Self.Controls.Add(Self.Button6);
  Self.Controls.Add(Self.Button9);
  Self.Controls.Add(Self.GroupBox1);
  Self.Name := 'TGomokuForm';
  Self.Text := 'Kółko i krzyżyk';
  Include(Self.Load, Self.TGomokuForm_Load);
  Self.GroupBox1.ResumeLayout(False);
  Self.ResumeLayout(False);
end;
{$ENDREGION}

procedure TGomokuForm.Dispose(Disposing: Boolean);
begin
  if Disposing then
  begin
    if Components <> nil then
      Components.Dispose();
  end;
  inherited Dispose(Disposing);
end;

constructor TGomokuForm.Create;
begin
  inherited Create;
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent;
  //
  // TODO: Add any constructor code after InitializeComponent call
  //
end;

procedure TGomokuForm.btnStart_Click(sender: System.Object; e: System.EventArgs);
var
  I : Integer;
begin
  { sprawdzenie, czy użytkownik podał imiona graczy }
  if (edtPlayer1.Text.Length = 0) or (edtPlayer2.Text.Length = 0) then
    MessageBox.Show('Podaj imiona graczy!');

  Gomoku.Player1 := edtPlayer1.Text;
  Gomoku.Player2 := edtPlayer2.Text;

  { rozpoczęcie gry }
  Gomoku.Start;

  { zmiana właściwości określającej rozpoczęcie gry }
  Started := True;

  lblResults.Text := 'Wynik: ' + Convert.ToString(Gomoku.Winnings[ptPlayer1]) + ' - ' + Convert.ToString(Gomoku.Winnings[ptPlayer2]);

  GroupBox1.Text := 'Tura dla: ' + edtPlayer1.Text;

  { wyczyszczenie zawartości przycisków }
  for I := 0 to Controls.Count -1 do
  begin
    if (Controls[i] is Button) then
      (Controls[i] as Button).Text := '';
  end;

  { pokazanie przycisku "Nowa gra" }
  btnNewGame.Visible := True;
end;

procedure TGomokuForm.Button3_Click(sender: System.Object; e: System.EventArgs);
var
  Symbol : Char;
  P : System.Drawing.Point;
begin
  { sprawdzenie, czy użytkownik nacisnął przycisk "Start" }
  if not Started then
    MessageBox.Show('Musisz nacisnąć przycisk "Start", aby rozpocząć gre!');

  Symbol := ' ';

  { nadaj wartości właściwościom, w zależności od symbolu użytkownika }
  case Gomoku.Active.&Type of
    ftCross:  Symbol := 'X';
    ftCircle: Symbol := 'O';
  end;

  { rzutowanie wartości właściwości Tag do rekordu P }
  P := Point((Sender as Button).Tag);

  try
    { ustawienie figury na polu }
    Gomoku.&Set(P.X, P.Y);

    { nadanie symbolu dla komponentu }
    (Sender as Button).Text := Symbol;
  except
    on EFieldNotEmpty do
      MessageBox.Show('To pole jest już zajęte!');
  end;

  { sprawdzenie, czy gracz wygrał }
  if Gomoku.Winner then
  begin
    MessageBox.Show('Wygrał gracz ' + Gomoku.Active.Name);

    lblResults.Text := 'Wynik: ' + Convert.ToString(Gomoku.Winnings[ptPlayer1]) + ' - ' + Convert.ToString(Gomoku.Winnings[ptPlayer2]);
  end
  else
  begin
    GroupBox1.Text := 'Tura dla: ' + Gomoku.Active.Name;
  end;
end;

procedure TGomokuForm.TGomokuForm_Load(sender: System.Object; e: System.EventArgs);
begin
  { utwórz egzemplarz klasy po starcie programu }
  Gomoku := TGomoku.Create;
end;

procedure TGomokuForm.edtNewGame_Click(sender: System.Object; e: System.EventArgs);
begin
  { wywołujemy metodę nakazującą rozpoczęcie nowej gry }
  Gomoku.NewGame;
  { wywołujemy procedurę zdarzeniową Click komponentu btnStart }
  btnStart_Click(Sender, E);
end;

end.

Grę w trakcie działania prezentuje rysunek 7.11.

7.11.jpg
Rysunek 7.11. Gra Kółko i krzyżyk

Biblioteka VCL/VCL.NET

Biblioteka VCL (w dalszej części tego podrozdziału będę używał terminu VCL w odniesieniu zarówno do biblioteki VCL, jak i VCL.NET) jest obecna jest w Delphi od samego początku. Zapisania obiektowo, w sposób hierarchiczny udostępnia setki klas mających ułatwić pracę programiście.

W tym podrozdziale omówię podstawowe zdarzenia oraz właściwości biblioteki VCL.

Klasa TApplication

Program wykorzystujący formularze posiada ukrytą zmienną @@Application@@, która wskazuje na klasę TApplication. Klasa ta odpowiada za działanie aplikacji, jej uruchamianie i zamykanie, obsługę wyjątków itp. Niestety, właściwości oraz zdarzenia tej klasy nie są widoczne w Inspektorze obiektów, więc operacji na klasie TApplication należy dokonywać bezpośrednio w kodzie programu.

Oto zawartość głównego pliku *.dpr zaraz po utworzeniu nowego projektu:

program Project1;

uses
  Forms,
  Unit1 in 'Unit1.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Wszystkie metody wywoływane w bloku begin..end znajdują się w klasie TApplication — to może świadczyć o tym, jak ważna z punktu widzenia VCL jest ta klasa.

Pierwszy wiersz, czyli instrukcja Initialize, powoduje zainicjalizowanie procesu działania aplikacji. Kolejna instrukcja — CreateForm — powoduje utworzenie formularza, a ostatnia — Run — uruchomienie aplikacji.

W dalszych punktach przedstawię najważniejsze właściwości, metody i zdarzenia klasy TApplication. Nie chcę jednak przekraczać pewnych ram i mówić o sprawach, o których Czytelnik dowie się w dalszej części książki — teraz zatem omówię tylko podstawowe właściwości, zdarzenia i metody.

Właściwości klasy TApplication

Właściwości klasy TApplication są ściśle związane z działaniem aplikacji i obsługą niektórych jej aspektów. Oto najważniejsze z nich…

Active

Właściwość @@Active@@ jest właściwością tylko do odczytu. Oznacza to, że nie można jej modyfikować, a jedynie odczytać jej wartość. Właściwość ta zwraca wartość True, jeżeli aplikacja jest aplikacją pierwszoplanową.

ExeName

@@ExeName@@ jest także właściwością tylko do odczytu. Określa ona ścieżkę do aplikacji wykonywalnej EXE.

  Label1.Caption := Application.ExeName;

Powyższy kod spowoduje wyświetlenie w etykiecie ścieżki do programu.

Pełny kod źródłowy programu wyświetlającego ścieżkę do aplikacji znajduje się na płycie CD-ROM, w katalogu listingi/7/ExeName.

ShowMainForm

Właściwość @@ShowMainForm@@ domyślnie posiada wartość True, co oznacza, że zostanie wyświetlony formularz główny. Nadając tej właściwości wartość False, blokujemy wyświetlenie formularza głównego:

begin
  Application.Initialize;
  Application.ShowMainForm := False; // nie wyświetlaj!
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end.
Title

Właściwość @@Title@@ określa tekst, który jest wyświetlony na pasku stanu obok ikony w czasie, gdy aplikacja jest zminimalizowana.

Application.Title := 'Nazwa programu';
Icon

Właściwość określa ikonę przypisaną do aplikacji, która będzie wyświetlona na belce tytułowej aplikacji. Właściwość wskazuje na obiekt TIcon. Oto przykład ustawienia ikony dla programu w zdarzeniu OnCreate formularza:

procedure TForm4.FormCreate(Sender: TObject);
var
  Icon : TIcon;
begin
  Icon := TIcon.Create;
  Icon.LoadFromFile('C:\default.ico');
  Application.Icon := Icon;
  Icon.Free;
end;

Przed przypisaniem do właściwości @@Icon@@ obiektu Application ikona zostaje załadowana z pliku default.ico.

Metody klasy TApplication

Oto parę opisów wybranych metod z klasy TApplication.

Minimize

Wywołanie metody Minimize spowoduje zminimalizowanie aplikacji do paska zadań. Wywołanie procedury jest proste:

Application.Minimize; // minimalizuj
Terminate

Wywołanie metody Terminate spowoduje natychmiastowe zamknięcie aplikacji. Inną funkcją zamykającą jest Close (z klasy TForm), ale zamyka ona jedynie formularz, a nie całą aplikację, dlatego jest zalecane używanie zamiast niej metody Terminate.

MessageBox

Funkcja MessageBox powoduje wyświetlenie okna informacyjnego, jest zatem jakby rozbudowaną funkcją ShowMessage, gdyż umożliwia ustalenie większej liczby parametrów.

procedure TForm1.FormCreate(Sender: TObject);
begin
  if Application.MessageBox('Uruchomiony program?',
  'Tak/Nie', MB_YESNO + MB_ICONINFORMATION) = id_No then Application.Terminate;
end;

Na podstawie powyższego kodu źródłowego po uruchomieniu programu zostanie wyświetlone okno ze stosownym pytaniem. Jeżeli użytkownik naciśnie przycisk Nie, program zostanie zamknięty.

ProcessMeessages

Podczas pisania programów w Delphi, Czytelnik zapewne nieraz jeszcze skorzysta z funkcji ProcessMessages. Owa metoda jest stosowana w trakcie wykonywania długich i czasochłonnych obliczeń (np. wykonanie dużej pętli), dzięki czemu nie powoduje zablokowania programu na czas wykonywania owych obliczeń.

Załóżmy, że w programie zastosowano dużą pętlę for, która wykona, powiedzmy, milion iteracji. Do czasu zakończenia działania pętli program będzie pozostawał zablokowany. Oznacza to, że użytkownik nie będzie miał żadnych możliwości zamknięcia programu czy zmiany położenia jego okna do czasu zakończenia działania pętli. W takim przypadku należy zastosować funkcję ProcessMessages:

for I := 0 to 1000000 do
begin
  Application.ProcessMessages;
  { wykonywanie instrukcji }
end;

Powyższy kod sprawia, ze wykonywanie pętli nie spowoduje unieruchomienia programu.

Moje wyjaśnienie dotyczące zasady działania metody ProcessMessages nie było zapewne zupełnie ścisłe i precyzyjne, gdyż wymaga wcześniejszego wytłumaczenia zasad działania mechanizmu zwanego komunikatami. Funkcja ProcessMessage powoduje bowiem przetworzenie wszystkich komunikatów w kolejce, a dopiero później następuje zwrócenie sterowania do aplikacji — dzięki temu program nie sprawia wrażenia „zawieszonego”.

Więcej informacji o obsłudze komunikatów w systemie Windows znajduje się w rozdziale 5. książki Delphi 7. Kompendium programisty, wydanej nakładem wydawnictwa Helion w 2003 r.

Restore

Wywołanie metody Restore spowoduje powrót aplikacji do normalnego stanu (jeżeli jest np. zminimalizowana).

Application.Restore; // przywróć normalne okno

Zdarzenia klasy TApplication

Być może Czytelnik pamięta, iż podczas lektury niniejszej książki miał okazję zapoznać się z działaniem zdarzenia o nazwie OnException z klasy TApplication. Obsługa zdarzeń klasy TApplication z poziomu Delphi powinna być więc znanym zagadnieniem. Tabela 7.2 przedstawia opis najważniejszych zdarzeń.

Tabela 7.2. Zdarzenia klasy TApplication

Zdarzenie Krótki opis
OnActivate Zdarzenie występuje w momencie, gdy aplikacja się uaktywnia.
OnDeactivate Kiedy aplikacja przestaje być aktywna, jest generowane zdarzenie OnDeactivate.
OnException O tym zdarzeniu była już mowa we wcześniejszych fragmentach tego rozdziału. Powoduje ono przechwycenie wszystkich wyjątków istniejących w programie.
OnIdle Występuje w momencie, gdy aplikacja przestaje być aktywna — nie wykonuje żadnych czynności.
OnMinimize Zdarzenie jest generowane w momencie, gdy aplikacja jest minimalizowana.
OnRestore To zdarzenie jest generowane, gdy aplikacja jest przywracana do normalnego stanu metodą Restore.
OnShortCut W momencie naciśnięcia przez użytkownika skrótu klawiaturowego jest generowane zdarzenie OnShortCut (występuje przed zdarzeniem OnKeyDown) .
OnShowHint Zdarzenie generowane w chwili pojawienia się dymka podpowiedzi.

Właściwości

Parę najbliższych stron poświęcę omówieniu podstawowych właściwości VCL, jakie można napotkać podczas pracy z Delphi. Nie będą to naturalnie wszystkie właściwości komponentów dostępne w Delphi — przedstawię tylko te podstawowe, dotyczące większości obiektów biblioteki VCL.

Align

Właściwość @@Align@@ służy do określenia położenia komponentu w formularzu. Dotyczy ona jedynie komponentów wizualnych. Wartość właściwości wybiera się z listy rozwijalnej inspektora obiektów. W tabeli 7.3. wymieniono wartości tej właściwości.

Tabela 7.3. Możliwe wartości właściwości Align

Wartość Opis
alBottom Komponent położony będzie u dołu formularza, niezależnie od jego wielkości.
alClient Obszar komponentu wypełni cały obszar formularza.
alCustom Położenie jest określane względem komponentu (formularza) macierzystego.
alLeft Obiekt położony będzie zawsze przy lewej krawędzi formularza lub komponentu macierzystego.
alNone Położenie nieokreślone (swobodne).
alRight Obiekt będzie zawsze położony przy prawej krawędzi formularza lub komponentu macierzystego.
alTop Komponent będzie położony u góry formularza.

Właściwość @@Align@@ może określać położenie komponentu względem formularza lub względem innego komponentu macierzystego. Takim komponentem jest TPanel, który jest rodzicem dla komponentu. Komponent TPanel, tak jak i wszystkie komponenty na nim umieszczone, stanowią jedną całość.

Anchors

Właściwość @@Anchors@@ można rozwinąć, klikając ikonę znajdującą się obok nazwy tej właściwości (rysunek 7.12).

7.12.jpg
Rysunek 7.12. Rozwinięta właściwość Anchors

Właściwość ta określa położenie komponentu względem komponentu-rodzica. Np. w przypadku, gdy właściwość akLeft gałęzi @@Anchors@@ ma wartość True, położenie komponentu po lewej stronie jest jakby „blokowane”. Podczas uruchomienia programu i rozciągania formularza komponent na nim umieszczony będzie zawsze położony w tym samym miejscu.

Warto to sprawdzić! Można zmienić wszystkie właściwości gałęzi @@Anchors@@ na False. Teraz należy uruchomić program i spróbować rozciągnąć lub zwężać formularz. Łatwo zauważyć, że komponent (np. TButton) będzie dopasowywał swe położenie do rozmiarów formularza.

Constraints

Po rozwinięciu tej gałęzi pojawią się właściwości @@MaxHeight@@, @@MinHeight@@, @@MaxWidth@@ i @@MinWidth@@. Określają one kolejno: maksymalną szerokość, minimalną szerokość, maksymalną wysokość oraz minimalną wysokość komponentu. Domyślnie wszystkie te właściwości posiadają wartość 0, co oznacza brak limitów. Aby zapewnić sobie możliwość zablokowania rozmiarów komponentu, należy pamiętać o gałęzi @@Constraints@@.

Cursor

Każdy komponent wizualny może posiadać osobny wskaźnik myszy. Oznacza to, że po naprowadzeniu wskaźnika myszy nad dany obiekt jego kształt zostanie zmieniony według właściwości @@Cursor@@ danego obiektu. Po rozwinięciu listy rozwijalnej obok nazwy każdego wskaźnika pojawi się jego podgląd (rysunek 7.13).

7.13.jpg
Rysunek 7.13. Lista wskaźników właściwości Cursor

DragCursor, DragKind, DragMode

Wszystkie te trzy właściwości są związane z funkcją Drag and Drop (ang. przeciągnij i upuść). Delphi umożliwia konstruowanie aplikacji, która obsługuje przeciąganie komponentów i umieszczanie ich w innych miejscach formularza.
@@DragCursor@@ określa kursor, który będzie określał stan przeciągania.

@@DragKind@@ określa, czy dany obiekt będzie mógł być przeciągany po formularzu, czy też będzie to miejsce tzw. dokowania (miejsce, gdzie można umieścić inny obiekt).

@@DragMode@@ określa, czy będzie możliwe przeciąganie danego komponentu. Ustawienie właściwości na dmManual wyłącza tę opcję. Z kolei ustawienie dmAutomatic udostępnia taką możliwość.

Font

Właściwość @@Font@@ dotyczy tylko komponentów wizualnych i określa czcionkę przez nie używaną. Gałąź @@Font@@ można rozwinąć, a następnie zdefiniować szczegółowe elementy, takie jak kolor, nazwa czcionki, wysokość czy styl (pogrubienie, kursywa, podkreślenie). Klasą TFont i związaną z nią właściwością @@Font@@ szczegółowo zajmiemy się w rozdziale na temat grafiki.

HelpContex, HelpKeyword, HelpType

Właściwości te są związane z plikiem pomocy. Większość starannie zaprojektowanych aplikacji w systemie Windows posiada plik pomocy — Delphi natomiast zawiera mechanizmy pozwalające na zintegrowanie pliku pomocy z aplikacją.

@@HelpContex@@ określa numer ID strony pomocy, której będzie dotyczyć dana kontrolka.

@@HelpKeyword@@ może zawierać słowo kluczowe określające daną kontrolkę. Łączy się to z ostatnią właściwością @@HelpType@@. Szukanie może się bowiem odbywać według ID (htContext) lub według słów kluczowych (htKeyword).

Hint, ShowHint

Właściwości typu @@Hint@@ są związane z tzw. dymkami podpowiedzi (ang. hint). Za ich pomocą można ustawić tekst podpowiedzi, który będzie wyświetlany po umieszczeniu wskaźnika myszy nad danym obiektem. Aby podpowiedź była wyświetlana, właściwość @@ShowHint@@ musi być ustawiona na True.

Z dymkami podpowiedzi wiąże się kilka dodatkowych właściwości klasy TApplication. Klasy TApplication nie trzeba tworzyć — jest ona deklarowana automatycznie. Wystarczy odwołać się do konkretnej pozycji:

Application.HintColor := clBlue;

Właściwość @@HintColor@@ pozwala na określenie koloru tła podpowiedzi.

Kolejna właściwość — @@HintHidePause@@ — określa czas w milisekundach (1 sek. = 1 000 milisekund), po którym wyświetlona podpowiedź zostanie ukryta.

@@HintPause@@ określa czas, po którym podpowiedź zostanie wyświetlona. Domyślną wartością jest 500 milisekund.

@@HintShortCuts@@ jest właściwością typu Boolean. Po zmianie tej właściwości na True wraz z podpowiedzią będzie wyświetlony skrót klawiaturowy wywołujący daną funkcję — np. „Wycina tekst do schowka (Ctrl+X)”.

Domyślna wartość kolejnej właściwości — @@HintShortPause@@ — to 50 milisekund. Właściwość ta określa czas, po jakim czasie zostanie wyświetlona podpowiedź kolejnej kontrolki, jeżeli wskaźnik myszy zostanie przemieszczony znad jednego komponentu na drugi (np. przeglądając pozycje menu lub przyciski pasków narzędziowych).

Podpowiedź będzie wyświetlana tylko wówczas, gdy właściwość @@ShowHint@@ danego obiektu będzie ustawiona na True.

Visible

Właściwość @@Visible@@ dotyczy jedynie komponentów wizualnych. Jeżeli jej wartość wynosi True (wartość domyślna), wówczas komponent będzie wyświetlany, natomiast jeżeli False — komponent podczas działania programu będzie ukryty.

Tag

Często można napotkać na właściwość @@Tag@@, gdyż jest obecna w każdym komponencie. Nie pełni ona żadnej funkcji — jest przeznaczona jedynie dla programisty do dodatkowego użycia. Można w niej przechowywać różne wartości liczbowe (właściwość @@Tag@@ jest typu Integer, natomiast w VCL.NET — typu Variant).

Zdarzenia

Parę najbliższych stron poświęcę omówieniu podstawowych zdarzeń VCL, jakie można napotkać podczas pracy z Delphi. Nie będą to naturalnie wszystkie zdarzenia komponentów dostępne w Delphi, gdyż to jest akurat specyficzną sprawą dla każdego komponentu.

OnClick

Zdarzenie OnClick występuje podczas kliknięcia przyciskiem myszy w obszarze danej kontrolki — jest to chyba najczęściej używane zdarzenie VCL, dlatego nie będę go szerzej opisywał. Mam nadzieję, że podczas lektury tej książki Czytelnik zorientuje się, do czego służy ta właściwość.

OnContextPopup

Delphi umożliwia tworzenie menu, w tym menu podręcznego (tzw. popup menu), rozwijanego po kliknięciu prawym przyciskiem myszy. To zdarzenie jest generowane właśnie wówczas, gdy popup menu zostaje rozwinięte.

Wraz z tym zdarzeniem programista otrzymuje informację dotyczącą położenia wskaźnika myszki (parametr @@MousePos@@) oraz tzw. uchwytu (o tym opowiem przy innej okazji).

Parametr @@MousePos@@ jest typu TPoint, a to nic innego jak zwykły rekord, zawierający dwie pozycje X i Y. A zatem jeżeli chcemy odczytać położenie wskaźnika myszy w poziomie, wystarczy odczytać je poprzez MousePos.X.

OnDblClick

Zdarzenie jest generowane podczas dwukrotnego kliknięcia danego obiektu. Obsługiwane jest tak samo jak zdarzenie OnClick — wraz ze zdarzeniem nie są dostarczane żadne dodatkowe parametry.

OnActivate, OnDeactivate

Te dwa zdarzenia są związane jedynie z oknami (formularzami). Występują w momencie, gdy okno stanie się aktywne (OnActivate) lub zostanie dezaktywowane (OnDeactivate).

OnClose, OnCloseQuery

Te dwa zdarzenia są związane również z formularzem, a konkretnie z jego zamykaniem. Dzięki zdarzeniu OnClose programista może zareagować na próbę zamknięcia okna. Wraz ze zdarzeniem jest dostarczany parametr Action, który określa konkretne zadanie do wykonania. Temu parametrowi można nadać wartości przedstawione w tabeli 7.4.

Tabela 7.4. Właściwości klasy TCloseAction

Wartość Opis
caNone Nic się nie dzieje — można zamknąć okno.
caHide Okno nie jest zamykane, a jedynie ukrywane.
caMinimize Okno jest minimalizowane zamiast zamykania
caFree Okno zostaje zwolnione, co w efekcie powoduje zamknięcie.

Zdarzenia OnCloseQuery można użyć, aby zapytać użytkownika, czy rzeczywiście ma zamiar zamknąć okno. Zdarzenie posiada parametr @@CanClose@@. Jeżeli nastąpi jego zmiana na False, okno nie zostanie zamknięte.

OnPaint

Zdarzenie OnPaint występuje zawsze wtedy, gdy okno jest wyświetlane i umieszczane na pierwszym planie. W zdarzeniu tym umieszcza się kod, którego zadaniem będzie „malowanie” w obszarze formularza.

OnResize

Zdarzenie OnResize występuje tylko wtedy, gdy użytkownik zmienia rozmiary formularza. Dzięki temu zdarzeniu można odpowiednio zareagować na zmiany lub nie dopuścić do nich.

OnShow, OnHide

Jak łatwo się domyśleć, te dwa zdarzenia informują o tym, czy aplikacja jest ukrywana czy pokazywana. Pokazanie lub ukrycie formularza jest dokonywane za pomocą metody Show lub Hide klasy TForm.

OnMouseDown, OnMouseMove, OnMouseUp, OnMouseWheel, OnMouseWheelDown, OnMouseWheelUp

Wszystkie wymienione zdarzenia są związane z obsługą myszy — są to kolejno: kliknięcie w obszarze kontrolki, przesunięcie wskaźnika myszy nad kontrolką, zwolnienie przycisku myszy, wykorzystanie rolki myszy, przesunięcie rolki w górę lub w dół.

Wraz z tymi zdarzeniami do aplikacji może być dostarczana informacja o położeniu wskaźnika myszy oraz o naciśniętym przycisku myszy (lewy, środkowy, prawy). Informacje te zawiera parametr Button klasy TMouseButton (tabela 7.5).

Tabela 7.5. Możliwe wartości klasy TMouseButton

Wartość Opis
mbLeft Naciśnięto lewy przycisk myszki.
mbMiddle Naciśnięto środkowy przycisk myszki.
mbRight Naciśnięto prawy przycisk myszki.

Wraz ze zdarzeniami obsługi myszy może być dostarczany również parametr Shift, który jest obecny także w zdarzeniach klawiaturowych (OnKeyUp, OnKeyDown). Wartości, jakie może posiadać parametr Shfit, są przedstawione w tabeli 7.6.

Tabela 7.6. Możliwe wartości klasy TShiftState

Wartość Opis
ssShift Klawisz Shift jest przytrzymany w momencie wystąpienia zdarzenia.
ssAlt Klawisz Alt jest przytrzymany w momencie wystąpienia zdarzenia.
ssCtrl Klawisz Ctrl jest przytrzymany w momencie wystąpienia zdarzenia.
ssLeft Przytrzymany jest również lewy przycisk myszy.
ssRight Przytrzymany jest także prawy przycisk myszy.
ssMiddle Przytrzymany jest środkowy przycisk myszy.
ssDouble Nastąpiło dwukrotne kliknięcie.

Zdarzenia związane z dokowaniem

Wspominałem już wcześniej o możliwości dokowania obiektów metodą przeciągnij i upuść. Związane jest z tym parę zdarzeń, które można napotkać, przeglądając listę z zakładki Events z Inspektora obiektów.

OnDockDrop

Zdarzenie OnDockDrop jest generowane w momencie, gdy użytkownik próbuje osadzić jakąś inną kontrolkę w obrębie danego obiektu.

OnDockOver

Zdarzenie to występuje w momencie, gdy jakaś inna kontrolka jest przeciągana nad danym obiektem.

OnStartDock

Zdarzenie występuje w momencie, gdy użytkownik rozpoczyna przeciąganie jakiegoś obiektu. Warunkiem wystąpienia tego zdarzenia jest ustawienie właściwości @@DragKind@@ na wartość dkDock.

OnStartDrag

Zdarzenie występuje tylko wówczas, gdy właściwość @@DragKind@@ komponentu jest ustawiona na dkDrag. To zdarzenie można wykorzystać w celu obsługi przeciągania obiektu.

OnEndDrag, OnEndDock

Pierwsze ze zdarzeń wykorzystuje się w przypadku, gdy należy zareagować na zakończenie procesu przeciągania, natomiast drugie można zastosować w przypadku zakończenia procesu przeciągnij i upuść.

OnDragDrop

Zdarzenie to jest generowane w momencie, gdy użytkownik zwalnia dane przeciągane metodą przeciągnij i upuść w danym komponencie.

OnDragOver

Zdarzenie to jest generowane wtedy, gdy nad danym komponentem użytkownik przeciąga wskaźnik myszy wraz z danymi.

Przykładowy program

Czytelników zainteresowanych metodą wymiany danych pomiędzy dwoma obiektami odsyłam do przykładowego programu znajdującego się na płycie CD-ROM, dołączonej do książki. Program jest umieszczony w katalogu listingi/7/Drag’n’Drop, a jego działanie prezentuje rysunek 7.14.

7.14.jpg
Rysunek 7.14. Działanie programu wykorzystującego metodę Drag and Drop

Program umożliwia wymianę danych metodą przeciągania pomiędzy obiektami TListBox. Możliwe jest także dowolne przemieszczanie komponentów — np. TButton, TLabel oraz umieszczanie ich w panelu (TPanel).

Aby przemieszczanie danych pomiędzy komponentami TListBox mogło dojść do skutku, właściwość DragMode musi być ustawiona na dmAutomatic. Równie dobrze można wywołać procedurę DragBegin komponentu TListBox w celu uruchomienia procesu przeciągania.

Programowanie w .NET

Środowisko .NET Framework, a konkretnie .NET Framework Class Library (biblioteka klasy) dostarcza projektantom klas umożliwiających programowanie graficznego interfejsu użytkownika (GUI), obsługę baz danych czy plików:

*System.Windows.Forms — przestrzeń nazw zawierająca klasy oraz interfejsy służące do projektowania interfejsu graficznego. Zawiera klasy reprezentujące podstawowe kontrolki interakcji z użytkownikiem (przyciski, listy rozwijalne, panele itp.) oraz chyba najważniejszą klasę obsługi formularza (System.Windows.Forms.Form).
*System.Data — przestrzeń nazw zawierająca klasy obsługi baz danych, takie jak MS SQL Server czy Oracle. Możliwa jest także obsługa technologii OLE DB lub ODBC. Tym zagadnieniem będziemy zajmować się w rozdziałach 16. i 17.
*System.XML — przestrzeń zawiera klasy umożliwiające obsługę plików XML (parsowanie, tworzenie, usuwanie, edycja). Zagadnienia związane z obsługą XML-a w Delphi omówię w rozdziale 18.
*System.IO — klasy zawarte w tej przestrzeni nazw służą do obsługi operacji wejścia-wyjścia. Dzięki nim można dodawać do swojej aplikacji obsługę plików, strumieni, katalogów itp. Tym zagadnieniem zajmiemy się w rozdziale 10.
*System.Web — to jeden z podstawowych komponentów środowiska .NET Framework, czyli ASP.NET. W tej przestrzeni nazw znajdują się klasy służące do obsługi ASP.NET oraz zawierające komponenty typu Web Forms. Technologią ASP.NET zajmiemy się w rozdziale 20.
*System.Reflection — przestrzeń nazw zapewniająca obsługę mechanizmu reflection. Nie będę tutaj zagłębiał się w szczegóły, szerzej o technologii reflection opowiem w rozdziale 8.
*System.Net — w tej przestrzeni nazw znajdują się klasy odpowiedzialne za obsługę różnych protokołów internetowych, takich jak HTTP, DNS czy IP.
*System.Security — przestrzeń nazw zawierająca mechanizmy zabezpieczeń, klasy implementujące różne algorytmy szyfrujące.

Wspólny model programowania

Niewątpliwą zaletą .NET jest pełna integracja i niezależność języków programowania. Do tej pory programiści mogli wybierać model programowania WinAPI lub z wykorzystaniem bibliotek, takich jak MFC czy VCL. Dzięki bibliotekom udostępnionym przez .NET nie trzeba się uczyć odrębnych struktur czy nazw funkcji. W każdym wykorzystanym języku, czy to będzie Delphi, czy C#, nazwy klas i przestrzeni nazw będą takie same. Różnicą jest jedynie sposób zapisu kodu źródłowego (składni) w poszczególnych językach. Przykładowo, zarówno w języku C#, Visual Basic.NET, jak i Delphi, można użyć klasy Console do wyświetlania tekstu na konsoli.

Podstawowa składnia języka C# została omówiona w dodatku A.

Klasa System.Object

Każdy typ w .NET jest obiektem. Bazową klasą dla każdego z typów jest System.Object. Nawet jeżeli nie określimy klasy bazowej naszej klasy, to kompilator automatycznie przyjmie, że jest to klasa System.Object. Owa klasa dostarcza podstawowych mechanizmów korzystania z klas — podstawowe metody zostały opisane w tabeli 7.7.

Tabela 7.7. Krótki opis metod używanych w klasie System.Object

Metoda Opis
Equals Porównuje, czy dwie instancje obiektu są takie same (mają taką samą zawartość).
ReferenceEquals
Porównuje, czy dwie instancje obiektu są tego samego typu.
GetHashCode Zwraca unikalny numer instancji obiektu.
GetType Zwraca informacje na temat obiektu: metody, właściwości itp.
ToString Znakowa reprezentacja obiektu — zwraca jego typ.

Na listingu 7.6 zaprezentowano przykład użycia funkcji z klasy System.Object. Kluczowym elementem w przykładzie jest klasa TDemoClass, zawierająca jedynie jedną metodę — ShowInfo.

Listing 7.6. Program prezentujący działanie klasy System.Object

program DemoApp;

{$APPTYPE CONSOLE}


type
  TDemoClass = class
    procedure ShowInfo;
  end;


var
  DemoClass : TDemoClass;

{ TDemoClass }

procedure TDemoClass.ShowInfo;
begin
  Console.WriteLine('GetHashType: ' + Convert.ToString(Self.GetHashCode));
  Console.WriteLine('GetType: ' + Self.GetType.GetMethod('ShowInfo').Name);
  Console.WriteLine('ToString: ' + Self.ToString);
  Console.Write('Equals: ');
  Console.Write(Self.Equals(Self));
end;

begin
  DemoClass := TDemoClass.Create; // tworzenie instancji klasy
  DemoClass.ShowInfo; // wywołanie funkcji
  DemoClass.Free;
  Readln;
end.

Rezultatem działania takiego programu będzie wyświetlanie w konsoli kilku informacji. W efekcie użytkownik powinien więc zobaczyć tekst taki jak poniżej:

GetHashType: 2
GetType: ShowInfo
ToString: DemoApp.TDemoClass
Equals: True

Więcej informacji na temat funkcji GetType, znajduje się w rozdziale 8. niniejszej książki.

Test

  1. Właściwość @@Tag@@ komponentów w VCL.NET jest typu:
    a) System.Object,
    b) Variant,
    c) Integer.
  2. Użycie konstruktora przed użyciem klasy w Win32 jest:
    a) możliwe, aczkolwiek niewymagane,
    b) konieczne,
    c) niekonieczne.
  3. Deklaracja pól i metod w sekcji strict private spowoduje, iż...
    a) ...pola i metody będą dostępne na zewnątrz klasy,
    b) ...pola i metody będą dostępne jedynie dla modułu, w którym znajduje się klasa,
    c) ...pola i metody nie będą w ogóle dostępne poza klasą.
  4. Użycie słowa kluczowego sealed powoduje:
    a) zaplombowanie klasy, niemożność dziedziczenia z danej klasy,
    b) uniemożliwia dodawanie metod do klasy,
    c) uniemożliwia tworzenie egzemplarza klasy.
  5. Właściwości klasy:
    a) mogą być tylko do odczytu,
    b) mogą być zarówno do odczytu i zapisu,
    c) mogą być zarówno do odczytu, jak i zapisu, ale tylko w .NET.
  6. Wywołanie metody Free spowoduje:
    a) wywołanie destruktora klasy,
    b) wywołanie destruktora klasy, ale tylko na platformie Win32,
    c) nie da żadnych efektów (metoda zachowana ze względów kompatybilności).
  7. Jaki rodzaj metod nie może zostać przedefiniowany?
    a) metody statyczne,
    b) metody dynamiczne,
    c) metody wirtualne.
  8. Rekordy w Delphi nie mogą zawierać:
    a) metod,
    b) pól,
    c) właściwości.

FAQ

Czy klasa może mieć więcej niż jeden konstruktor?
Tak, oczywiście. Konstruktory mogą również podlegać procesowi przeładowania.

Co to jest hermetyzacja?
Hermetyzacja jest procesem ukrywania szczegółów implementacyjnych przed użytkownikiem danej klasy. Klasa może posiadać wiele pól oraz metod, ale tych najważniejszych, dzięki którym klasa komunikuje się z użytkownikiem, może być tylko kilka.

Czym jest obiekt?
W tej książce bardzo często używam słowa obiekt i klasa zamiennie. W Delphi obiektami są także komponenty VCL.NET, ale gwoli ścisłości obiekt jest instancją danej klasy.

W grze Kółko i krzyżyk zaimplementowano pętlę czyszczącą planszę. Czemu czyści jedynie planszę, mimo iż na formularzu są obecne inne komponenty Button?

Wszystko dlatego, iż przyciski Start oraz Nowa gra były umieszczone w obrębie komponentu GroupBox. Pętla natomiast odwoływała się do kontrolek umieszczonych bezpośrednio na formularzu.

Jak wyświetlić nazwy wszystkich komponentów umieszczonych na formularzu?

W grze Kółko i krzyżyk pętla analizowała tylko te komponenty, które były umieszczone bezpośrednio na formularzu. Nie uwzględniała tych, które były umieszczone np. na komponencie GroupBox. Poniżej przedstawiam rozwiązanie, które umożliwia wyświetlenie wszystkich komponentów na liście kontrolki ListBox. W tym celu należy zastosować technikę zwaną rekurencją. Rekurencja to procedura lub funkcja, która wywołuje samą siebie. Oto przykład procedury zdarzeniowej (dla Windows Forms), która wyświetla nazwy wszystkich kontrolek umieszczonych na formularzu:

procedure TWinForm2.Button2_Click(sender: System.Object; e: System.EventArgs);

  procedure PrintControl(Component : Control);
  var
    i : Integer;
  begin
    for I := 0 to Component.Controls.Count -1 do
    begin
      ListBox1.Items.Add(System.Object(Component.Controls[i].Name));

      PrintControl(Component.Controls[i]);
    end;
  end;

begin
  PrintControl(Self);
end; 

Podsumowanie

Być może lektura tego rozdziału była dla Czytelnika ciężkim zadaniem. Nie da się ukryć, że programowanie obiektowe może być trudne do zrozumienia, jednak tak jest tylko na początku. Z czasem programiści oswajają się z tą techniką programowania. Dobry skądinąd język C nie ma możliwości programowania obiektowego, co — według mnie — jest jego mankamentem.
Pewnym jest to, że programowanie obiektowe odgrywa dużą rolę w procesie tworzenia aplikacji. Po lekturze tego rozdziału Czytelnik przede wszystkim powinien być w stanie określić, czym są klasy i jak ich używać. Na samym początku można pisać swoje programy w sposób strukturalny, gdyż umiejętność programowania obiektowego przydaje się przede wszystkim podczas budowania większych aplikacji.

.. [#] Niekiedy stosuje się określenia collector lub collection — jest to kwestia dowolna. Słowo collector oznacza odśmiecacz, a collection — odśmiecanie.

[[Delphi/Vademecum|Spis treści]]

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

2 komentarzy

To jest fragment ksiazki (Vademeum programisty) ktora po kawalku wrzucam na 4programmers.net. A jako ze pisalem to ladnych pare lat temu, ze nie pamietam ile czasu trwalo pisanie tego rozdzialu ;)

Wow ! Ile to pisałeś ?