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).
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 TextBox
— KeyDown
, 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.
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).
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.
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).
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;
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).
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.
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).
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.
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.
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).
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).
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.
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
- Właściwość @@Tag@@ komponentów w VCL.NET jest typu:
a)System.Object
,
b)Variant
,
c)Integer
. - Użycie konstruktora przed użyciem klasy w Win32 jest:
a) możliwe, aczkolwiek niewymagane,
b) konieczne,
c) niekonieczne. - 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ą. - 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. - 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. - 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). - Jaki rodzaj metod nie może zostać przedefiniowany?
a) metody statyczne,
b) metody dynamiczne,
c) metody wirtualne. - 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.
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ś ?