Rozdział 7. Tworzenie własnych komponentów
Możliwości oferowane przez rodzime komponenty biblioteki VCL są naprawdę imponujące i dla wielu programistów - projektantów stanowią one materiał w zupełności wystarczający do tworzenia skomplikowanych aplikacji. Ze względu jednak na ogrom rozmaitych zastosowań technologii informatycznych zupełnie naturalną może okazać się sytuacja, kiedy to żaden z istniejących komponentów nie będzie w pełni przydatny do realizacji założonego celu projektowego. W tym kontekście ogromną zaletą C++Buildera (jak również Delphi) jest nie tylko „otwartość” biblioteki VCL, polegająca na możliwości wzbogacania jej o komponenty pochodzące z niezależnych źródeł, lecz przede wszystkim zestaw oferowanych przez IDE mechanizmów ułatwiających tworzenie nowych komponentów we własnym zakresie, przy minimalnym (w porównaniu do osiąganych korzyści) wysiłku.
W rozdziale tym przedstawimy - na przykładach - najważniejsze zagadnienia związane z tworzeniem nowych komponentów, a więc wzbogacanie klasy bazowej o niezbędne właściwości, metody i zdarzenia, różnicowanie zachowań komponentu w zależności od uwarunkowań zewnętrznych (np. uruchomiona aplikacja kontra etap projektowania), rejestrację gotowych komponentów w palecie, i oczywiście związane z tym mechanizmy IDE.
Rozdział ten nie wyczerpuje bynajmniej szerokiej tematyki definiowania nowych komponentów. Nie zagłębialiśmy się na przykład w mechanizmy warstwy Win32 API, o których zainteresowany Czytelnik przeczytać może w rozdziale 14. Mimo iż komponenty rejestrowane w środowisku IDE rezydują w ramach konkretnych pakietów, pominęliśmy również tematykę tych ostatnich —piszemy o nich nieco obszerniej w rozdziale 11. Szczegóły wielu interesujących zagadnień opisane są także w plikach systemu pomocy C++Buildera i Windows SDK.
Podstawy tworzenia komponentów
Poszczególne komponenty VCL różnią się od siebie pod względem architektury, genealogii, spełnianych funkcji itp., więc przy tworzeniu nowego komponentu sprawą niezmiernie istotną jest wybór właściwej klasy bazowej.
Komponenty niewidoczne (non-visual) definiowane są zazwyczaj na bazie klasy TComponent. Jako klasa bazowa dla wszystkich komponentów zapewnia ona środki dla integracji komponentów ze środowiskiem IDE, jak również dla strumieniowania ich właściwości (informacje na temat strumieniowania i obiektów trwałych znajdziesz w rozdziale 6.). Podstawowym przeznaczeniem komponentów niewidocznych jest obudowywanie fragmentów kodu spełniających określone funkcje, nie pozostające w bezpośredniej relacji do konkretnej reprezentacji wizualnej. Przykładem takiej funkcji może być przechwytywanie komunikatów o błędach i kierowanie ich do jakiejś kontrolki „tekstowej” w rodzaju TMemo czy TRichEdit, bądź zapisywanie ich w pliku tekstowym; spełniający tę funkcję konkretny komponent wykonuje swoje czynności niejako „w tle”, bez manifestowania się w konkretnej postaci graficznej.
Komponenty okienkowe (windowed) wywodzą się z klasy TWinControl. Pojawiają się one w czasie wykonania aplikacji jako elementy jej interfejsu graficznego i zapewniają interakcję z użytkownikiem, w postaci np. wyboru określonej pozycji z listy bądź wpisywania zawartości tekstowej w pola edycyjne. Jakkolwiek możliwe jest wyprowadzanie komponentów okienkowych bezpośrednio z klasy TWinControl, C++Builder oferuje specjalnie w tym celu klasę TCustomControl.
Komponenty graficzne (graphic) tym różnią się od komponentów okienkowych, iż nie posiadają uchwytu (handle), reprezentującego okno systemu Windows; nie umożliwiają więc bezpośredniej interakcji z użytkownikiem, mogą jednak reagować na komunikaty stanowiące skutek określonych zachowań użytkownika, np. klikania myszą. Brak wspomnianego uchwytu ma również pewne pozytywne konsekwencje w postaci mniejszego zapotrzebowania na zasoby systemu. Komponenty graficzne budowane są najczęściej na bazie klasy TGraphicControl.
Rozszerzanie możliwości klasy bazowej
Tworzenie nowych komponentów drogą „rozbudowy” komponentów istniejących jest naturalną konsekwencją obiektowej natury komponentów VCL - dziedziczenie i polimorfizm czynią kod obiektu możliwym do wielokrotnego użycia (ang. reusable) w tym sensie, iż elementy klasy bazowej - pola, właściwości, metody - nie wymagają ponownego programowania, lecz są z tej klasy dziedziczone przez klasę pochodną. Posiada to niebagatelne konsekwencje, chociażby w kontekście poprawności tego kodu - dziedzicząc bowiem należycie przetestowany kod klasy bazowej dziedziczy się jednocześnie jego wiarygodność; nie da się tego oczywiście powiedzieć o kodzie tworzonym „od zera”.
Okazuje się, iż tworzenie nowego komponentu niekoniecznie musi wiązać się z definiowaniem nowych pól, właściwości i metod; równie częstą przesłanką w tym względzie jest zmiana standardowych ustawień danego komponentu. Wyjaśnijmy tę prostą koncepcję na równie prostym przykładzie.
Najpowszechniej bodaj używanymi komponentami we wszystkich niemal aplikacjach są etykiety (TLabel), służące zazwyczaj do opisywania innych komponentów. Umieszczając na formularzu nowy komponent TLabel i spoglądając na jego właściwości, zauważamy, iż jego tytuł (Caption) tożsamy jest z nazwą (Name) i wypisany domyślną, ośmiopunktową czcionką MS Sans Serif w kolorze czarnym. Zmiana tych domyślnych ustawień - stosownie do wymagań użytkownika tworzącego aplikację - nie stanowi oczywiście żadnego problemu, staje się jednak po trosze uciążliwa, gdy dokonywać jej trzeba permanentnie, kilkanaście - kilkadziesiąt razy w każdej nowo tworzonej aplikacji. Można zaoszczędzić sobie tej fatygi, definiując nowy komponent, różniący się od komponentu bazowego TLabel jedynie wartościami początkowymi niektórych właściwości; wartości te nadawane będą stosownym właściwościom w treści konstruktora.
Pokażemy teraz, jak łatwo jest wykonać tę czynność w praktyce. Rozpoczynamy od wybrania opcji Component|New Component z menu głównego IDE. W wyświetlonym oknie dialogowym wybieramy żądaną klasę bazową (w polu Ancestor type) - w tym przypadku TLabel; najprościej wpisać w tym celu kilka początkowych liter tej nazwy (np. „TLa”), i dokonać rozwinięcia listy skojarzonej z polem edycyjnym. Po kliknięciu żądanej pozycji wspomnianej listy wypełnione zostaną dodatkowo pola: Class Name, Palette Page i Unit file name, zawierające (odpowiednio): proponowaną nazwę klasy tworzonego komponentu, nazwę strony palety komponentów, na której nowy komponent zostanie umieszczony, a także nazwę i lokalizację pliku modułu źródłowego. Zmieniając nazwę tworzonej klasy - w tym przypadku na TStyleLabel - spowodujemy automatyczną zmianę proponowanej nazwy modułu źródłowego.
Gdy klikniemy przycisk OK, C++Builder dokona automatycznego wygenerowania modułu źródłowego związanego z tworzonym komponentem (patrz wydruki 7.1 i 7.2). Jedyną niezbędną z naszej strony ingerencją w wygenerowany tekst modułu będzie uzupełnienie konstruktora komponentu o instrukcje dokonujące niezbędnych ustawień początkowych, co na wydruku 7.2 zaznaczone zostało tekstem wytłuszczonym - domyślną czcionką dla nowo umieszczanych na formularzach komponentów TStyleLabel będzie 12-punktowa wytłuszczona czcionka Verdana.
Wydruk 7.1. StyleLabel.h - wygenerowany plik nagłówkowy dla nowo tworzonego komponentu
//---------------------------------------------------------------------------
#ifndef StyleLabelH
#define StyleLabelH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <StdCtrls.hpp>
//---------------------------------------------------------------------------
class PACKAGE TStyleLabel : public TLabel
{
private:
protected:
public:
__fastcall TStyleLabel(TComponent* Owner);
__published:
};
//---------------------------------------------------------------------------
#endif
Wydruk 7.1. StyleLabel.cpp - wygenerowany tekst modułu źródłowego dla nowo tworzonego komponentu
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "StyleLabel.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TStyleLabel *)
{
new TStyleLabel(NULL);
}
//---------------------------------------------------------------------------
__fastcall TStyleLabel::TStyleLabel(TComponent* Owner)
: TLabel(Owner)
{
Font->Name = "Verdana";
Font->Size = 12;
Font->Style = Font->Style << fsBold;
}
//---------------------------------------------------------------------------
namespace Stylelabel
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TStyleLabel)};
RegisterComponents("Samples", classes, 0);
}
}
//---------------------------------------------------------------------------
Komponent TStyleLabel jest już gotowy do zainstalowania w palecie. Fizycznej instalacji dokonuje się, wybierając opcję Component|Install Component z menu głównego IDE. Komponenty C++Buildera rezydują wewnątrz pakietów; użytkownik ma do wyboru instalację w istniejącym pakiecie (służy do tego karta Into Existing Package) lub we własnym pakiecie o wybranej nazwie i stosownym opisie (co umożliwia karta Into New Package). Kliknięcie przycisku OK spowoduje rozpoczęcie przez C++Builder właściwych czynności instalacyjnych - należy wówczas odpowiedzieć twierdząco na zapytanie o utworzenie nowego pakietu i poczekać na wyświetlenie komunikatu końcowego, zawierającego nazwę utworzonego pakietu i nazwę klasy utworzonego komponentu.
Podobną w swej istocie przesłanką tworzenia nowych komponentów jest nie tyle zmiana ich właściwości domyślnych, ile zmiana kategorii widoczności poszczególnych właściwości, a dokładniej - zmiana zestawu właściwości opublikowanych (published), a więc dostępnych z poziomu inspektora obiektów. Przykładem takiej operacji może być ukrycie (na etapie projektowania) właściwości Items komponentu TListBox, która to właściwość stanie się tym samym dostępna jedynie z poziomu kodu aplikacji. Operację tę można najłatwiej wykonać, wybierając w charakterze klasy bazowej TCustomListBox - zestaw jej opublikowanych właściwości ogranicza się do opublikowanych właściwości dziedziczonych z klasy TWinControl; użytkownik ma więc pełną swobodę wyboru (do opublikowania) właściwości nowo definiowanych. W taki właśnie sposób zdefiniowano klasę TListBox.
Założenia projektowe
Podobnie jak w przypadku tworzenia aplikacji, tak i przy tworzeniu komponentów należy wykazać się dostateczną dozą wyobraźni w zakresie przyszłych tendencji rozwojowych powstającego produktu. I tak na przykład, decydując się na tworzenie szerokiego wachlarza rozmaitych list przeglądowych, podobnych do TListBox i bazujących na materiałach źródłowych jakiegoś specyficznego rodzaju, nie należy pochopnie wyprowadzać każdej z tych list (jako komponentu) bezpośrednio z klasy TListBox, lecz zastanowić się nad jakimś specyficznym komponentem wyprowadzonym właśnie z TListBox (lub TCustomListBox), obejmującym pewne cechy wspólne dla wszystkich tworzonych (teraz i w przyszłości) list przeglądowych. Oczywiście, nie będąc jasnowidzem, trudno jest owe cechy a priori bezbłędnie przewidzieć - i właśnie w tym celu niezbędna jest wspomniana przed chwilą wyobraźnia.
Niezwykle pomocnym w zrozumieniu zależności pomiędzy komponentami okaże się z pewnością schemat biblioteki VCL (VCL Chart), stanowiący wyposażenie C++Buildera. Dzięki niemu widoczne staje się natychmiast dziedziczenie poszczególnych właściwości, modyfikowanie zachowań wynikających z przedefiniowywania metod wirtualnych itp.; wiedza ta może być wzbogacona przez lekturę kodu źródłowego biblioteki (w języku Object Pascal), dostępnego w plikach *.pas, znajdujących się w drzewie podkatalogu Source lokalnej instalacji C++Buildera.
Tworzenie komponentów niewidocznych
Świat komponentów VCL zbudowany jest na trzech solidnych fundamentach: właściwościach, zdarzeniach i metodach. W tym podrozdziale zajmiemy się ich rolą w funkcjonowaniu poszczególnych komponentów i organizowaniu pomiędzy nimi współpracy warunkującej efektywne funkcjonowanie budowanych aplikacji.
Właściwości
Właściwości komponentów podzielić można na dwie grupy, w zależności od ich dostępności dla programisty operującego z poziomu IDE: właściwości opublikowane (published) dostępne są poprzez inspektora obiektów, właściwości niepublikowane (non-published) dostępne są tylko z poziomu kodu źródłowego.
Właściwości niepublikowane
Spójrzmy na poniższą deklarację klasy:
Wydruk 7.3. Metody służące do odczytu i modyfikacji prywatnego pola klasy
class LengthClass
{
private:
int FLength;
public:
LengthClass(void){}
~LengthClass(void){}
int GetLength(void);
void SetLength(int pLength);
void LengthFunction(void);
}
Klasa LengthClass posiada prywatną zmienną FLength, której wartość może być odczytywana i zmieniana za pomocą metod (odpowiednio) GetLength() i SetLength(), na przykład w ten sposób:
Wydruk 7.4. Dostęp do prywatnego pola klasy
LengthClass Rope;
Rope.SetLength(15);
....
int NewLength = Rope.GetLength();
Powyższy kod nie odwołuje się bezpośrednio do pola FLength, wykorzystując w zamian wspomniane metody. W złożonej aplikacji może to ujemnie wpływać na czytelność kodu - związek pomiędzy polem FLength a metodami GetLength() i SetLength() nie jest widoczny na pierwszy rzut oka, ponadto z samą czynnością odczytywania i zmieniania wartości czegokolwiek bardziej kojarzy się operator przypisania niż wywołanie funkcji. C++Builder (podobnie jak Delphi) udostępnia w związku z tym mechanizm właściwości (ang. properties) - deklaracja z wydruku 7.3 może zostać przepisana w sposób następujący:
Wydruk 7.5. Właściwość organizująca dostęp do prywatnego pola klasy
class LengthClass2
{
private:
int FLength;
public:
LengthClass2(void){}
~LengthClass2(void){}
void LengthFunction(void);
__property int Length = {read = FLength, write = FLength};
}
Dostęp do pola FLength będzie mieć wówczas postać bardziej intuicyjną:
Wydruk 7.6. Zmiana pola klasy za pomocą właściwości
LengthClass Rope;
Rope.Length = 15;
....
int NewLength = Rope.Length;
Zamiast wywołań funkcji mamy do czynienia z odczytem i zapisem „czegoś” o nazwie Length, co w swej naturze przypomina pole klasy, lecz polem bynajmniej nie jest: słowo kluczowe __property oznacza, iż mamy do czynienia z właściwością. Klauzula read w definicji właściwości Length informuje, iż jej odczyt powinien być fizycznie zrealizowany jako odczyt zmiennej FLength; analogicznie ma się sprawa z zapisem, za który odpowiedzialna jest klauzula write. Na pierwszy rzut oka nie wydaje się to szczególnie odkrywcze, lecz w porównaniu z bezpośrednim dostępem do pola FLength różni się co najmniej pod dwoma względami:
w przeciwieństwie do pola, właściwość można uczynić tylko odczytywalną, opuszczając klauzulę write w jej definicji;
sposób dostępu do wybranych elementów klasy oddzielony jest od ich fizycznej implementacji, ta ostatnia może więc być zmieniana bez potrzeby jakichkolwiek modyfikacji w kodzie odwołującym się do obiektów tej klasy.
Mechanizm właściwości umożliwia coś więcej. Otóż właściwość jest ze swej natury tworem dość abstrakcyjnym - dopiero jej definicja wiąże ją z „konkretnym” elementem klasy, jakim na wydruku 7.5 jest pole FLength; w związku z tym odczyt lub zmiana jej wartości mogą wymagać czynności bardziej skomplikowanych niż tylko odczyt lub zamiana któregoś z pól. Elementem wskazywanym przez klauzule read i write może więc być także metoda klasy, jak na wydruku 7.7:
Wydruk 7.7. Właściwość wykorzystująca metody dostępowe
class LengthClass3
{
private:
int FLength;
int GetLength(void);
void SetLength(int pLength);
public:
LengthClass3(void){}
~LengthClass3(void){}
void LengthFunction(void);
__property int Length = {read = GetLength, write = SetLength};
}
Fraza read = GetLength oznacza tu, iż wynikiem odczytu właściwości Length jest wynik zwrócony przez metodę GetLength(), tak więc na przykład instrukcja:
int NewLength = Rope.Length;
realizowana jest fizycznie jako:
int NewLength = Rope.GetLength();
Podobnie przypisanie do właściwości nowej wartości jest tylko symbolicznym oznaczeniem czegoś, co tak naprawdę jest wywołaniem metody - zgodnie z definicją na wydruku 7.7 przypisanie:
Rope.Length = 15;
realizowane jest fizycznie jako:
Rope.SetLength(15);
gdzie przypisywana wartość jest parametrem wywołania metody specyfikowanej w klauzuli write. Ze względu na rolę pełnioną w klasie LengthClass3 metody GetLength() i SetLength() nazywane są metodami dostępowymi (ang. access methods), organizują one bowiem dostęp do właściwości Length.
„Obliczenia” wykonywane przez metody dostępowe mogą być niekiedy dość złożone - przykładowo zmiana właściwości komponentu bazodanowego, reprezentującej konkretną bazę danych, określoną za pomocą aliasu, wymaga rozłączenia się z dotychczasową bazą i przyłączenia do innej. Czynności te realizowane są jednak „w tle” przez metody dostępowe - programista ogranicza się tylko do przypisania właściwości nowej nazwy aliasu.
Typy właściwości
Właściwości klas mogą być dowolnego typu, na przykład: int, bool, short itp., mogą także same być klasami. W tym ostatnim przypadku muszą one czynić zadość dwóm wymaganiom: po pierwsze, jeżeli dana właściwość ma mieć charakter trwały - tj. zapisywana ma być w strumieniu - musi się ona wywodzić (bezpośrednio lub pośrednio) z klasy TPersistent, definiującej niezbędne ku temu mechanizmy; po drugie, jeżeli klasa właściwości deklarowana jest w formie zapowiedzi (ang. forward), zapowiedź ta musi zawierać klauzulę: _declspec(delphiclass).
Spójrzmy na wydruk 7.8, ilustrujący typową deklaracje zapowiadającą - zapowiadana klasa nie jest wykorzystywana przez żadną właściwość:
Wydruk 7.8. Deklaracja zapowiadająca
class MyClass;
class PACKAGE MyComponent : public TComponent
{
private:
MyClass *FMyClass;
....
};
class MyClass : public TPeristent
{
public:
__fastcall MyClass(void){}
};
Jeżeli jednak zdefiniujemy właściwość typu MyClass, w deklaracji zapowiadającej musi pojawić się klauzula __declspec(delphiclass), co ilustruje wydruk 7.9:
Wydruk 7.9. Zapowiedź klasy wykorzystywanej jako typ właściwości
class __declspec(delphiclass) MyClass;
class PACKAGE MyComponent : public TComponent
{
private:
MyClass *FMyClass;
....
__published:
__property MyClass *Class1 = {read = FMyClass, write = FMyClass};
};
class MyClass : public TPeristent
{
public:
__fastcall MyClass(void){}
};
Makro PACKAGE jest tutaj odpowiednikiem klauzuli __declspec(package), oznaczającej, iż deklarowana klasa jest komponentem zdolnym do przechowywania w ramach pakietów.
Właściwości opublikowane
Aby właściwość komponentu dostępna była za pośrednictwem inspektora obiektów, należy ją wpierw opublikować, czyli umieścić jej definicję w sekcji __published deklaracji klasy. Opublikowane właściwości są oczywiście także dostępne w kodzie programu, na równi z pozostałymi właściwościami, manipulowanie ich wartościami za pomocą inspektora obiektów jest jednak na ogół wygodniejsze niż wpisywanie równoważnych temu instrukcji w kod programu. Właściwości opublikowane są także domyślnie utrwalane w strumieniu, w wyniku czego nadane im ostatnio (na etapie projektowania) wartości pozostają aktualne przy następnym załadowaniu projektu do IDE lub uruchomieniu skompilowanej aplikacji.
Aby więc opublikować właściwość Length klasy z wydruku 7.7, należy przesunąć jej definicję z sekcji public do sekcji __published:
Wydruk 7.10. Opublikowanie właściwości
class PACKAGE LengthClass : public TComponent
{
private:
int FLength;
int GetLength(void);
void SetLength(int pLength);
public:
__fastcall LengthClass(TObject *Owner) : TComponent(Owner) {}
__fastcall ~LengthClass(void){}
void LengthFunction(void);
__published:
__property int Length = {read = Getlength, write = Setlength};
}
Opublikowana właściwość nie będzie jednak widoczna w oknie inspektora obiektów, jeżeli w jej definicji brak będzie klauzuli write. Nie będzie można wówczas zmienić jej wartości, a więc udostępnianie jej przez inspektora obiektów i tak byłoby bezcelowe. Można jednak pogodzić dwa pozornie sprzeczne wymagania, pozostawiając właściwość widoczną dla inspektora obiektów i jednocześnie chroniąc ją przed zmianą wartości - inspektor obiektów nie wnika bowiem w to, co dzieje się wewnątrz metody dostępowej, można więc wskazać w klauzuli write funkcję o pustej treści. Wydruk 7.11 ilustruje pewną odmianę tej idei - metoda dostępowa SetVersion() właściwości Version nadaje tej ostatniej wartość określoną a priori, nie zaś wskazaną przez użytkownika; parametr tej metody nie jest w ogóle wykorzystywany, a więc dla uniknięcia ostrzeżeń ze strony kompilatora został on „wykomentowany”. W efekcie właściwość Version posiada wciąż niezmienną wartość określoną przez stałe MajorVersion i MinorVersion.
Wydruk 7.11. Nietypowa metoda dostępowa
const int MajorVersion = 1;
const int MinorVersion = 0;
class PACKAGE LengthClass : public TComponent
{
private:
AnsiString FVersion;
int FLength;
int GetLength(void);
void SetLength(int pLength);
void SetVersion(AnsiString /* pVersion */ )
{FVersion = AnsiString(MajorVersion) + ""."" +
AnsiString(MinorVersion);}
public:
__fastcall LengthClass(TObject *Owner) : TComponent(Owner)
{SetVersion("""");}
__fastcall ~LengthClass(void){}
void LengthFunction(void);
__published:
__property int Length = {read = Getlength, write = Setlength};
__property AnsiString Version = {read = FVersion, write = SetVersion};
}
Właściwości tablicowe
Typ właściwości nie musi być typem skalarnym - może on być również tablicą. Przykładem właściwości tablicowej jest właściwość Lines komponentu TMemo, reprezentująca jego zawartość tekstową w podziale na poszczególne wiersze. Właściwość tablicową definiuje się niemal tak samo jak właściwość skalarną - z tą różnicą, iż należy wskazać fakt indeksowania i określić typ indeksu; w przeciwieństwie bowiem do „zwykłych” tablic indeksy właściwości niekoniecznie muszą być liczbami całkowitymi. Wydruk 7.12 przedstawia przykład definicji dwóch właściwości tablicowych, z którym jedna indeksowana jest łańcuchami AnsiString.
Wydruk 7.12. Przykład właściwości tablicowych
class PACKAGE TStringAliasComponent : public TComponent
{
private:
TStringList RealList;
TStringList AliasList;
__AnsiString __fastcall GetStringAlias(AnsiString RawString);
AnsiString __fastcall GetRealString(int Index);
void __fastcall SetRealString(int Index, AnsiString Value);
public:
__property AnsiString AliasString[AnsiString RawString] =
{read = GetStringAlias};
__property AnsiString RealString[int Index] = {read=GetRealString,
write=SetRealString};
}
Tablicowy charakter właściwości odzwierciedla się także w postaci jej metod dostępowych. Obydwie metody - „odczytująca” i „zapisująca” - posiadają dodatkowy parametr określający, który „element” właściwości należy odczytać lub zmodyfikować; parametr ten specyfikowany jest na pierwszej pozycji. Oto definicje deklarowanych powyżej metod dostępowych właściwości RealString:
Wydruk 7.13. Metody dostępowe właściwości tablicowej
AnsiString __fastcall TStringAliasComponent::GetRealString(int Index)
{
if(Index > (RealList->Count -1))
return """";
return RealList->Strings[Index];
}
void __fastcall TStringAliasComponent::SetRealString(int Index,
AnsiString Value)
{
if((RealList->Count - 1) < Index)
RealList->Add(Value);
else
RealList->Insert(Index, Value);
}
Wydruk 7.13 stanowi połączenie oryginalnych listingów 9.13 i 9.14
Odwołania do właściwości tablicowej mają identyczną postać jak odwołania do „zwykłej” tablicy - przy założeniu, iż zmienna StringAlias1 wskazuje na obiekt typu TStringAliasComponent, poniższa sekwencja dokonuje wyczyszczenia pierwszego elementu właściwości RealString, o ile jest on identyczny z drugim:
if (StringAlias1->RealString[0] == StringAlias1->RealString[1])
StringAlias1->RealString[0] := "";
Nietrudno zauważyć, iż właściwość RealString zdefiniowana została w oparciu o listę łańcuchów RealList - elementy tej listy odpowiadają wprost poszczególnym elementom właściwości, z jedną drobną różnicą: „odczyt” poza zakresem listy skutkuje zwróceniem pustego łańcucha, zaś „zapis” poza zakresem realizowany jest jako dopisanie podanego łańcucha na końcu listy. Widać wyraźnie, iż posługiwanie się właściwością może mieć charakter nieco bardziej elastyczny niż bezpośredni dostęp do pola, na którym właściwość ta bazuje.
Każdy z łańcuchów przechowywanych w liście RealList posiada swój odpowiednik - alias - w liście AliasList; równoważne łańcuchy przechowywane są w obydwu listach na tych samych pozycjach. Aby więc odnaleźć alias określonego łańcucha, należy wpierw odnaleźć jego pozycję w liście RealList (używając metody TStringList::IndexOf()), a następnie odczytać analogiczną pozycję w liście AliasList. W taki właśnie sposób zaimplementowana została tablicowa właściwość AliasString, której indeks jest właśnie łańcuchem; wydruk 7.14 przedstawia jej „odczytującą” metodę dostępową. Jeżeli łańcuch podany jako indeks nie występuje w liście RealList lub nie posiada swego odpowiednika w liście AliasList, to przyjmuje się, że jest on sam dla siebie aliasem.
Wydruk 7.14. Metoda dostępowa właściwości tablicowej indeksowanej za pomocą łańcucha
AnsiString __fastcall TStringAliasComponent::GetStringAlias(
AnsiString RawString)
{
int RawStringIndex;
RawStringIndex = RealList->IndexOf(RawString);
if((RawStringIndex == -1) || (RawStringIndex > (AliasList->Count-1)))
return RawString;
return AliasList->Strings[RawStringIndex];
}
Instrukcja znajdująca alias przykładowego łańcucha ma wobec tego następującą postać:
AniString MyAlias = StringAlias1->AliasString["My original string"];
Domyślne wartości właściwości
Niektóre właściwości posiadają tę osobliwą cechę, iż ich wartość rzadko kiedy różna jest od pewnej wartości domyślnej - przykładem tego jest właściwość Tag (klasy TComponent), której wartość rzadko odbiega od (zwyczajowej) wartości 0. Dla tego rodzaju właściwości opłaca się zastosować alternatywny sposób jej strumieniowania - zamiast konsekwentnego zapisywania jej do strumienia przy każdym zapisie obiektu, należy zapisywać ją tylko wówczas, gdy jej wartość (w momencie zapisu) różna jest od wartości domyślnej. Jednocześnie należy inicjować tę właściwość jej wartością domyślną w konstruktorze klasy - jeżeli została ona uprzednio zapisana do strumienia, to odczytana z tegoż strumienia wartość zastąpi wartość domyślną nadaną w konstruktorze; jeżeli właściwość nie posiada swego obrazu w strumieniu, pozostanie ona ze swą wartością domyślną.
Domyślną wartość właściwości określa się w jej definicji za pomocą klauzuli default - przykładowo wspomniana właściwość TComponent::Tag definiowana jest następująco:
__property int Tag = {read=FTag, write=FTag, default=0};
Brak klauzuli default oznacza, iż z daną właściwością nie jest skojarzona żadna wartość domyślna. Konsekwencje klauzuli default (tj. istnienie wartości domyślnej i jej wartość) dziedziczone są z klasy bazowej w jej klasach pochodnych. Możliwa jest zmiana dziedziczonej wartości domyślnej za pomocą ponownej specyfikacji dziedziczonej właściwości (w klasie pochodnej) z użyciem klauzuli default; można także anulować sam fakt istnienia wartości domyślnej, opatrując dziedziczoną właściwość klauzulą nodefault.
Decyzja o zapisywaniu konkretnej właściwości do strumienia może być również uzależniona od spełnienia pewnego warunku; warunek ten specyfikowany jest przy użyciu klauzuli stored i może mieć postać stałej true lub false nazwy pola boolowskiego albo nazwy bezparametrowej metody, zwracającej wynik typu Boolean. Oto przykład:
Wydruk 7.15. Przykład użycia klauzuli stored
class PACKAGE LengthClass : public TComponent
{
protected:
int FProp;
bool StoreProperty(void);
__published:
__property int AlwaysStore = {read = FProp, write = FProp, stored = true};
__property int NeverStore = {read = FProp, write = FProp, stored = false};
__property int SimetimesStore = {read = FProp, write = FProp,
stored = StoreProperty};
}
Kolejność tworzenia właściwości
Jeżeli poszczególne właściwości są od siebie w jakiś sposób uzależnione, istotna jest kolejność ich odczytywania ze strumienia. Kolejność ta jest identyczna z kolejnością dyrektyw __property definiujących poszczególne właściwości. Na poniższym wydruku poszczególne właściwości inicjalizowane są w kolejności: PropA, PropB, PropC - jeżeli więc na przykład metoda SetPropB() korzystać będzie z właściwości PropC, to właściwość PropB może nie zostać prawidłowo zainicjowana.
Wydruk 7.16. Kolejność inicjowania właściwości
class PACKAGE SampleComponent : public TComponent
{
private:
int FPropA;
bool FPropB;
String FProC;
void __fastcall SetPropB(bool pPropB);
void __fastcall SetPropC(String pPropC);
public:
__property int PropA = {read = FPropA, write = FPropA};
__property bool PropB = {read = FPropB, write = SetPropB};
__property String PropC = {read = FPropC, write = SetPropC};
}
Zdarzenia
W kategoriach terminologii komponentów VCL zdarzeniem (ang. event) nazywamy wywołanie określonej metody w reakcji na wystąpienie pewnej okoliczności, którą może być otrzymanie przez komponent komunikatu od Windows, zaistnienie wyjątku lub wykonanie przez aplikację pewnej szczególnej czynności.
W charakterze przykładowego komponentu operującego zdarzeniami rozpatrzmy deklarowany na wydruku 7.17 komponent TTraverseDir. Dokonuje on iterowania po wszystkich podkatalogach wskazanego katalogu, umożliwiając wykonanie dla każdego z odwiedzanych katalogów pewnej czynności określonej przez użytkownika. Zgodnie z poprzednim akapitem wywołanie metody realizującej ową czynność będzie zdarzeniem generowanym w reakcji na określoną akcję, jaką jest przejście do innego katalogu.
Wydruk 7.17. Deklaracja właściwości zdarzeniowej
class PACKAGE TTraverseDir : public TComponent
{
private:
AnsiString FCurrentDir;
TNotifyEvent *FOnDirChanged;
public:
__fastcall TTraverseDir(TObject *Owner) : TComponent(Owner){
FOnDirChanged = 0;}
__fastcall ~TTraverseDir(void){}
__fastcall Execute();
__published:
__property AnsiString CurrentDir = {read = FCurrentDir};
__property TNotifyEvent OnDirChanged = {read = FOnDirChanged,
write = FOnDirChanged};
}
Funkcja, której wywołanie jest istotą wspomnianego zdarzenia - zwana z tego względu funkcją obsługi zdarzenia (ang. event handler) lub krótko funkcją zdarzeniową - jest tutaj wskazywana przez pole FOnDirChanged „obudowane” właściwością OnDirChanged; właściwość ta, jako opublikowana, pojawi się na stronie Events inspektora obiektów. Oto schemat ilustrujący wywoływanie wskazywanej funkcji przy każdej zmianie katalogu bieżącego:
Wydruk 7.18. Generowanie zdarzenia OnDirChanged w reakcji na zmianę katalogu bieżącego
void __fastcall TTraverseDir::Execute(void)
{
while (<istnieje nieodwiedzony katalog>)
{
<przejdź do tego katalogu>
<podstaw nazwę katalogu bieżącego pod pole FCurrentDir>
// katalog został zmieniony, wygeneruj zdarzenie
if (FOnDirChanged) // czy określono funkcję obsługi zdarzenia?
{
FOnDirChanged(this);
}
....
}
....
}
Funkcja obsługi zdarzenia typu TNotifyEvent posiada pojedynczy parametr wskazujący obiekt, w związku z którym zdarzenie to jest generowane:
typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender);
Gdy klikniemy dwukrotnie pozycję odpowiadającą zdarzeniu OnDirChanged (na stronie Events inspektora obiektów) edytor kodu wygeneruje automatycznie stosowny szkielet funkcji obsługującej to zdarzenie:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender)
{
}
Parametr Sender umożliwia rozróżnienie pomiędzy poszczególnymi komponentami w sytuacji, gdy kilka z nich posługuje się tą samą funkcją zdarzeniową; nazwa bieżącego katalogu znajduje się w polu FCurrentDir wskazywanego obiektu:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender)
{
AnsiString ThisDirectory = dynamic_cast<TTraverseDir*>(Sender)->FCurrentDir;
if (Sender == Traverse1)
{
akcja charakterystyczna dla komponentu Traverse1
}
else
{
akcja stosowna dla innych komponentów
}
}
Definiowanie własnych typów zdarzeń
Pokażemy teraz, jak zdefiniować własny typ zdarzenia. Procedura obsługi naszego nowego zdarzenia posiadać będzie drugi parametr typu bool; przy wejściu do funkcji zdarzeniowej będzie on miał wartość false - zmiana tej wartości na true spowoduje, iż iteracja zostanie przerwana, tj. następne katalogi nie będą już odwiedzane.
Zdefiniujmy więc najpierw rzeczony typ zdarzenia:
typedef void __fastcall (__closure *TDirChangeEvent)
(System::TObject* Sender, bool &Abort)
a następnie zmodyfikujmy odpowiednio deklarację klasy TTraverseDir:
Wydruk 7.19. Zdarzenie o typie definiowanym przez użytkownika
typedef void __fastcall (__closure *TDirChangedEvent)(
System::TObject* Sender, bool &Abort)
class PACKAGE TTraverseDir : public TComponent
{
private:
AnsiString FCurrentDir;
TDirChangedEvent *FOnDirChanged;
public:
....
__published:
__property TDirChangedEvent OnDirChanged = {read = FOnDirChanged,
write = FOnDirChanged};
}
Gdy klikniemy teraz odpowiednią pozycję na stronie Events inspektora obiektów, wygenerowany szkielet funkcji zdarzeniowej przedstawiał się będzie następująco:
void __fastcall TTraverseDir::Traverse1DirChanged(TObject* Sender, bool &Abort)
{
}
Sama iteracja stanie się również trochę bardziej skomplikowana:
Wydruk 7.20. Iterowanie po katalogach z możliwością przerwania przez obsługę zdarzenia
void __fastcall TTraverseDir::Execute(void)
{
bool AbortIteration = false;
while (<istnieje nieodwiedzony katalog>)
{
<przejdź do tego katalogu>
<podstaw nazwę katalogu bieżącego pod pole FCurrentDir>
// katalog został zmieniony, wygeneruj zdarzenie
if (FOnDirChanged) // czy określono funkcję obsługi zdarzenia?
{
FOnDirChanged(this, AbortIteration);
if (AbortIteration)
{
<wykonaj specyficzne czynności związane z przerwaniem iteracji>
break;
}
}
....
}
....
}
Metody
Metody komponentów VCL nie różnią się niczym od metod pozostałych klas. Są one funkcjami C++, spełniającymi pewne specyficzne zadania związane z komponentem; ze względu jednak na prostotę konstrukcji komponentu i wygodę jego użytkownika używanie metod powinno być podporządkowane następującym regułom:
funkcjonowanie komponentu nie może być uzależnione od konieczności jawnego wywołania (przez użytkownika) którejś z metod - wszelkie niezbędne czynności inicjalizacyjne powinny być wykonane w ramach konstruktora;
nie należy wymagać od użytkownika wywoływania metod komponentu w jakiejś określonej kolejności - nawet jeżeli kolejność taka podyktowana jest względami logiki działania komponentu, należy obsłużyć każdą sytuację, w której nie będzie ona przestrzegana. I tak na przykład skierowanie do bazy danych zapytania SQL musi być poprzedzone jej otwarciem, nie można jednak zakładać, iż użytkownik wywołując metodę generującą to zapytanie, na pewno wywoła wcześniej metodę ustanawiającą połączenie z bazą - jeżeli więc zapytanie skierowane zostanie do bazy nie otwartej, należy fakt ten odpowiednio obsłużyć, na przykład generując wyjątek lub też automatycznie otwierając bazę;
metody zmieniające stan komponentu powinny każdorazowo sprawdzać, czy zmiana taka jest w danej chwili dozwolona w kontekście innych operacji wykonywanych przez komponent.
Ogólnie rzecz biorąc, preferowanym środkiem komunikowania się komponentu z otoczeniem są jego właściwości, jak wiadomo również korzystające z metod. Przykładowo komponenty bazodanowe wywodzące się z klasy TDataSet posiadają właściwość Active, określającą, czy nawiązane jest połączenie z bazą danych (true), czy też nie (false). Przypisywanie tej właściwości wartości true i false ma dokładnie taki sam skutek, jak wywoływanie metod (odpowiednio) Open() i Close(), tak więc dwie poniższe instrukcje są sobie równoważne:
Database1->Active = true;
Database1->Open();
podobnie jak instrukcje:
Database1->Active = false;
Database1->Close();
Decydując się na używanie właściwości Active, można obejść się bez metod Open() i Close(), z drugiej strony te ostatnie nie mogą zastąpić właściwości Active (bo jak wówczas sprawdzić stan połączenia komponentu z bazą?).
Metody publiczne i chronione
Metody komponentów tworzone są najczęściej jako publiczne (public) i chronione (protected). Metody publiczne przeznaczone są do bezpośredniego wywoływania przez użytkownika; jako że może on realizować te wywołania w dowolnym czasie, należy upewnić się, iż dana metoda nie powoduje długotrwałego zajęcia procesora, bez chwili „oddechu” dla systemu Windows. Takie „blokujące” metody nie tylko paraliżują mobilność aplikacji, lecz równie często powodują irytację użytkownika, chcącego przerwać czasochłonny proces i nie mogącego tego uczynić; metoda TTraverseDir::Execute z wydruku 7.20 wykorzystuje bardzo prosty mechanizm zapobiegający takiej niekorzystnej sytuacji. Równie nieskomplikowanie zapobiec można sparaliżowaniu aplikacji, wywołując cyklicznie metodę Application->ProcessMessages().
Metody chronione przeznaczone są w zasadzie na użytek komponentów pochodnych; użytkownik nie ma do nich bezpośredniego dostępu. Zapobiega to wywoływaniu tych metod w sposób nieuprawniony, czyli bez spełnienia koniecznych ku temu warunków. Jeżeli możliwości jakiejś chronionej metody predestynują ją do udostępnienia jej użytkownikowi końcowemu, należy udostępnienie to zrealizować w sposób pośredni, „obudowując” tę chronioną metodę inną metodą publiczną, stwarzającą swej partnerce warunki niezbędne do jej wywołania lub przynajmniej badającą spełnienie tych warunków.
Jeżeli metoda chroniona jest metodą dostępową którejś (którychś) właściwości, powinna być deklarowana jako wirtualna. Umożliwi to modyfikację lub rozszerzenie jej możliwości w klasach pochodnych. Przykładem takiej wirtualnej metody chronionej jest metoda Loaded() klasy TComponent wywoływana automatycznie po odczytaniu całego obrazu komponentu ze strumienia. Jej pierwowzór w klasie TComponent ogranicza się do wyzerowania znacznika csLoading, oznaczającego, iż właśnie trwa odczyt komponentu:
procedure TComponent.Loaded;
begin
Exclude(FComponentState, csLoading);
end;
natomiast w klasach pochodnych jej treść jest już cokolwiek bardziej złożona. Jako że poszczególne elementy komponentu - pola, metody i zdarzenia - są od siebie w różnym stopniu uzależnione, wszelkie czynności „globalne” o charakterze np. weryfikacyjnym nie powinny być podejmowane przed kompletnym załadowaniem jego obrazu ze strumienia. Dla upewnienia się co do spełnienia tego warunku można badać wspomniany znacznik csLoading, można również zdefiniować w tym celu własną zmienną boolowską. Poniższy wydruk przedstawia typowy przykład przedefiniowania metody Loaded() - po zrealizowaniu wszystkich czynności związanych z ukończeniem ładowania komponentu bazowego realizowane są czynności specyficzne dla klasy pochodnej, m.in. ustawienie wspomnianej zmiennej.
Wydruk 7.21. Przedefiniowanie wirtualnej metody chronionej
class PACKAGE TAliasComboBox : public TSmartComboBox
{
private:
bool IsLoaded;
protected:
virtual void __fastcall Loaded(void);
}
....
void __fastcall TAliasComboBox::Loaded(void)
{
TComponent::Loaded();
if(!ComponentState.Contains(csDesigning))
{
IsLoaded = true;
GetAliases();
}
}
Definiowanie wyjątków związanych z komponentem
Przy tworzeniu własnych komponentów nie można oczywiście zapominać o prawidłowej obsłudze różnego rodzaju błędnych sytuacji. Owymi „błędnymi sytuacjami” są przede wszystkim wyjątki pojawiające się podczas realizacji kodu związanego z komponentem, są nimi także rozmaite przypadki niespełnienia wymaganych warunków, wykryte przez metody komponentu.
Najprostszą formą obsługi wyjątku zaistniałego w trakcie realizacji metody komponentu jest ponowienie tegoż wyjątku, by mógł być on obsłużony w ramach aplikacji wykorzystującej komponent. Rozwiązanie takie nie zapewnia jednak należytej kontroli nad samym komponentem; bardziej eleganckim wyjściem wydaje się więc przekształcenie wyjątku w zdarzenie, czyli sprowadzenie obsługi wyjątku do generowania pewnego specyficznego zdarzenia. Właściwą obsługę wyjątku zapewniałby wówczas sam użytkownik w ramach obsługi tegoż zdarzenia; nieprzypisanie wspomnianemu zdarzeniu obsługującej go funkcji mogłoby wówczas skutkować wspomnianym ponowieniem wyjątku.
Należy przestrzec jednak przed nadużywaniem tej koncepcji - zbyt duża liczba zdarzeń generowanych w reakcji na występowanie wielu wyjątków z pewnością uczyniłaby korzystanie z komponentu zbyt niewygodnym.
Przesłanką generowania wspomnianego zdarzenia mogą być zresztą nie tylko zaistniałe wyjątki, lecz i innego rodzaju sytuacje, błędne z punktu widzenia logiki komponentu. W charakterze przykładu rozpatrzmy komponent MultiQuery, służący do generowania ciągu zapytań SQL pod adresem bazy danych. Zapytania zgrupowane są w liście Queries typu TStrings, a ich źródłem może być np. komponent TMemo wypełniony przez użytkownika konkretną zawartością; samo generowanie zapytań jest sprawą metody Execute(). Przykładowa sekwencja instrukcji korzystających z komponentu mogłaby wyglądać na przykład tak:
MultiQuery->Queries->Assign(Memo1->Lines);
MultiQuery->Execute();
To wszystko wydaje się proste tak długo, jak długo zapytania realizowane są bezbłędnie; co zrobić jednak, jeżeli w czasie realizacji któregoś z nich wystąpi wyjątek? Całkowite „zepchnięcie” jego obsługi na użytkownika nie wydaje się najwłaściwsze z punktu widzenia realizacji całego scenariusza; należy raczej wygenerować pewne specyficzne zdarzenie, w ramach którego użytkownik będzie miał możliwość (między innymi) zadecydowania o przerwaniu lub kontynuowaniu tego scenariusza (czego przykład widzieliśmy już przy okazji iterowania po strukturze katalogów).
Metoda Execute() komponentu MultiQuery posiłkuje się wywołaniami metody ExecuteItem(), generującej pojedyncze zapytanie określone przez parametr wywołania równy indeksowi w liście Queries. Użytkownik również posiada dostęp do metody ExecuteItem(), którą może wywołać z dowolną wartością parametru - także ujemną lub wykraczającą poza rozmiar listy. Jest to sytuacja ewidentnie błędna z punktu widzenia logiki komponentu, z punktu widzenia biblioteki VCL wszystko jest jednak w porządku dopóty, dopóki nie nastąpi odwołanie do samej listy Queries. W przypadku stwierdzenia niepoprawności parametru należy więc takiemu odwołaniu zapobiec, generując w zamian pewien specyficzny wyjątek, będący sygnałem o błędzie; wyjątek ten w dalszej kolejności będzie najprawdopodobniej konwertowany do zdarzenia w sposób wyżej opisany.
Rozpoczniemy od zdefiniowania klasy reprezentującej nasz specyficzny wyjątek:
Wydruk 7.22. Deklaracja klasy-wyjątku użytkownika
class EMultiQueryIndexOutOfBounds : public Exception
{
public:
__fastcall EMultiQueryIndexOutOfBounds(const AnsiString Msg) :
Exception(Msg){}
};
Następnie uzupełnimy metodę ExecuteItem() o niezbędny test parametru i generowanie (w razie potrzeby) zdefiniowanego wyjątku:
Wydruk 7.23. Generowanie wyjątku użytkownika
void __fastcall TMultiQuery::ExecuteItem(int Index)
{
if(Index < 0 || Index >= Queries->Count)
throw EmultiQueryIndexOutOfBounds;
....
}
W oryginale jest błąd (listing 9.24) - w trzeciej instrukcji zamiast
„Index >= Queries->Count”
jest
„Index > Queries->Count”.
Błąd ten jest powtórzony na listingu 9.25
Opisane postępowanie jest jak najbardziej celowe w odniesieniu do uruchomionej aplikacji; na etapie projektowania stosowniejszym - i wygodniejszym dla użytkownika - rozwiązaniem wydaje się jednak wyświetlenie odpowiedniego komunikatu (zamiast generowania wyjątku). Zmodyfikowaną metodę ExecuteItem(), czyniącą zadość temu wymaganiu, przedstawia wydruk 7.24.
Wydruk 7.24. Obsługa błędnej sytuacji na etapie projektowania
void __fastcall TMultiQuery::ExecuteItem(int Index)
{
if(Index < 0 || Index >= Queries->Count)
{
if(ComponentState.Contains(csDesigning))
ShowMessage ("Indeks zapytania poza zakresem");
else
throw EmultiQueryIndexOutOfBounds;
}
....
}
Odpowiednik powyższego listingu w oryginale (9.25) jest bezsensowny.
Przestrzenie nazw - dyrektywa namespace
W przypadku wykorzystywania w danym projekcie komponentów pochodzących od różnych wytwórców prawdopodobne staje się wystąpienie konfliktu nazw. Oto przykład użycia dwóch różnych komponentów „zegarowych”:
// w module od pierwszego wytwórcy:
const bool Mode12; // tryb 12- lub 24-godzinny
class PACKAGE TClock1 : public TComponent
{
}
// w module od drugiego wytwórcy:
const bool Mode12; // tryb 12- lub 24-godzinny
class PACKAGE TClock2 : public TComponent
{
}
Nazwa Mode12 jest tu użyta w dwóch różnych znaczeniach.
W języku C++ środkiem zapobiegającym konfliktom nazw są przestrzenie nazw (ang. namespaces) identyfikowane przez słowo kluczowe namespace.
Powróćmy na chwilę do wydruku 7.1, przedstawiającego wygenerowany automatycznie kod modułu związanego z nowo tworzonym komponentem TStyleLabel; znajduje się tam następująca definicja przestrzeni nazw:
Wydruk 7.25. Przykładowa definicja przestrzeni nazw
namespace Stylelabel
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TStyleLabel)};
RegisterComponents("Samples", classes, 0);
}
}
Aby wykluczyć niebezpieczeństwo konfliktu nazw pochodzących z modułów źródłowych różnych komponentów, należy deklarować tworzony komponent w jego własnej przestrzeni nazw. Należy w tym celu „otoczyć” przestrzenią nazw deklarację klasy komponentu w pliku nagłówkowym - wydruk 7.26 pokazuje, jak zrobić to z komponentem TStyleLabel zadeklarowanym w pliku StyleLabel.h:
Wydruk 7.26. Deklarowanie komponentu w jego własnej przestrzeni nazw
//---------------------------------------------------------------------------
#ifndef StyleLabelH
#define StyleLabelH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <StdCtrls.hpp>
//---------------------------------------------------------------------------
namespace MJF01_NStyleLabel
{
class PACKAGE TStyleLabel : public TLabel
{
private:
protected:
public:
__fastcall TStyleLabel(TComponent* Owner);
__published:
} ;
}
//---------------------------------------------------------------------------
#endif
Deklaracja przestrzeni nazw otacza tu cały kod występujący po dyrektywach #include; identyfikator przestrzeni nazw utworzony został na podstawie akronimu firmy - autora komponentu oraz nazwy klasy poprzedzonej literą „N”. Sama nazwa klasy mogłaby nie wystarczyć - identyfikator przestrzeni nazw musi być bowiem unikatowy z globalnego punktu widzenia.
Obsługa komunikatów
Biblioteka zasadniczo VCL bierze na siebie całą obsługę komunikatów, konwertując większość z nich do postaci bardziej strawnych dla użytkownika zdarzeń. Nie oznacza to jednak, iż programista tworzący aplikację za pomocą C++Buildera (lub Delphi) nie ma do tych komunikatów dostępu - jeżeli wymagają tego specyficzne względy projektu, programista może wziąć na siebie obsługę niektórych komunikatów.
Jako przykład rozpatrzmy kontrolkę TStringGrid wzbogaconą o możliwość „przeciągania” do niej plików z Eksploratora Windows; tak zmodyfikowaną kontrolkę opatrzyliśmy nazwą TSuperStringGrid.
Jeżeli dane okno zarejestrowane jest w systemie jako zdolne przyjmować „przeciągane” pliki (o tym dokładniej za chwilę), w momencie „upuszczenia” nad nim ikony reprezentującej plik przeciągnięty z okna Eksploratora następuje wygenerowanie komunikatu WM_DROPFILES; szczegółowa informacja niesiona przez ten komunikat reprezentowana jest przez strukturę TWMDropFiles zdefiniowaną w pliku messages.hpp. Właściwa obsługa komunikatu jest zadaniem metody WmDropFiles naszej kontrolki. Kod źródłowy uwzględniający owe trzy czynniki obsługi - nazwę komunikatu, nazwę struktury i nazwę metody - generowany jest w wyniku następującej sekwencji, zwanej popularnie mapą komunikatu:
Wydruk 7.27. Przykładowa mapa komunikatu
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles)
END_MESSAGE_MAP(TStringGrid)
Wykorzystane makra BEGIN_MESSAGE_MAP, MESSAGE_HANDLER i END_MESSAGE_MAP zdefiniowane są w pliku nagłówkowym sysmac.h w sposób następujący:
Wydruk 7.28. Makra dokonujące mapowania komunikatu
#define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) \
{ \
switch (((PMessage)Message)->Msg) \
{
//-----------------------------------------------------------
#define END_MESSAGE_MAP(base) default: \
base::Dispatch(Message); \
break; \
} \
}
//-----------------------------------------------------------
#define MESSAGE_HANDLER VCL_MESSAGE_HANDLER
#define VCL_MESSAGE_HANDLER(msg,type,meth) \
case msg: \
meth(*((type *)Message)); \
break;
//-----------------------------------------------------------
Jak łatwo się zorientować, mapa komunikatów ma postać instrukcji switch i obejmować może więcej niż jeden komunikat, tj. wykorzystywać kilka wywołań makra MESSAGE_HANDLER pomiędzy zamykającymi nawiasami BEGIN_MESSAGE_MAP i END_MESSAGE_MAP.
Wspomniana metoda WmDropFiles, odpowiedzialna za obsługę komunikatu WM_DROPFILES, zdefiniowana jest następująco:
Wydruk 7.29. Obsługa „upuszczenia” pliku
void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message)
{
char buff[MAX_PATH];
HDROP hDrop = (HDROP)Message.Drop;
POINT Point;
int NumFiles = DragQueryFile(hDrop, -1, NULL, NULL);
TStringList *DFiles = new TStringList;
DFiles->Clear();
DragQueryPoint(hDrop, &Point);
for(int i = 0; i < NumFiles; i++)
{
DragQueryFile(hDrop, i, buff, sizeof(buff));
DFiles->Add(buff);
}
DragFinish(hDrop);
// teraz DFiles zawiera listę nazw przeciągniętych plików
delete DFiles;
}
Wyjaśnienie wszystkich szczegółów związanych z przeciąganiem plików pomiędzy oknami wykracza poza ramy tego rozdziału - naszym zadaniem było jedynie zaprezentowanie sposobu przechwytywania komunikatów Windows przez komponenty VCL. Zainteresowany Czytelnik znajdzie w systemie pomocy dokładny opis wszystkich funkcji wykorzystywanych w kodzie na wydruku 7.29.
Jak już wcześniej wspominaliśmy, aby w momencie upuszczenia pliku nad oknem generowany był komunikat WM_DROPFILES, okno to musi zostać zarejestrowane w systemie jako predestynowane do tej funkcji. Rejestracji takiej (oraz wyrejestrowania) dokonuje funkcja API o nazwie DragAcceptFiles(). Jej pierwszy parametr jest uchwytem odnośnego okna, ukrywającym się pod właściwością Handle każdej kontrolki okienkowej; drugi parametr określa, czy mamy do czynienia z rejestracją (true - okno ma przyjmować przeciągnięte pliki), czy z wyrejestrowaniem (false). W naszej kontrolce rodzaj żądania określony jest przez właściwość CanDropFiles, bazującą na prywatnym polu FCanDropFiles, zatem wywołanie funkcji rejestrującej ma postać:
DragAcceptFiles(Handle, FCanDropFiles)
i następuje przy każdej zmianie wspomnianej właściwości, jak również przy tworzeniu i ładowaniu formularza.
Etap projektowania a etap wykonania
Pożądane zachowanie poszczególnych komponentów różni się bardzo często w zależności od tego, czy uczestniczą one w wykonaniu skompilowanej aplikacji (runtime stage), czy też aplikacja ta znajduje się właśnie na etapie projektowania (designtime stage). Elementem każdego komponentu, umożliwiającym odróżnienie etapu wykonania od poszczególnych stadiów etapu projektowania, jest jego właściwość zbiorowa (Set) o nazwie ComponentState. Znaczenie jej poszczególnych elementów wyjaśnia pokrótce tabela 7.1.
Tabela 7.1. Elementy właściwości TComponent::ComponentState
Znacznik |
Znaczenie |
csAncestor |
Komponent znajduje się na formularzu stanowiącym klasę bazową dla aktualnego formularza. Znacznik ten jest ustawiany tylko łącznie ze znacznikiem csDesigning; do manipulowania nim służy metoda TComponent::SetAncestor(). |
csDesigning |
Komponent uczestniczy w aplikacji znajdującej się na etapie projektowania. Znacznik ten jest ustawiany i zerowany przez metodę TComponent::SetDesigning(). |
csDesignInstance |
Komponent jest komponentem najwyższego poziomu (root) z punktu widzenia projektanta formularzy - czyli na przykład ramką (TFrame) , w której umieszcza się aktualnie inne komponenty, lecz nie funkcjonującą jako odrębny komponent umieszczony na formularzu. Znacznik ten jest ustawiany tylko łącznie ze znacznikiem csDesigning;. do manipulowania nim służy metoda TComponent::SetDesignInstance(). Nowość w wersji 5. C++Buildera. |
csDestroying |
Komponent znajduje się w fazie destrukcji. Znacznik ten ustawiany jest przez metodę TComponent::Destroying(). |
csFixups |
Komponent powiązany jest z nie załadowanym jeszcze komponentem na innym formularzu. Znacznik ten zerowany jest przez globalną funkcję GlobalFixupReferences(), gdy opracowane zostaną wszystkie zależności pomiędzy komponentami aplikacji. |
csFreeNotification |
Komponent będzie wysyłał do innych formularzy powiadomienie o własnej destrukcji. Znacznik ten ustawiany jest przez metodę TComponent::FreeNotification(). Nowość w wersji 5. C++Buildera. |
csInline |
Komponent jest komponentem najwyższego poziomu umieszczonym na formularzu i podlegającym modyfikacji w czasie projektowania. Znacznik ten używany jest do identyfikacji zagnieżdżonych ramek (TFrame) w czasie ładowania i zapisywania komponentu; modyfikowany jest przez metodę TComponent::SetInline(), jest również ustawiany w metodzie TReader::ReadComponent(). Nowość w wersji 5. C++Buildera. |
csLoading |
Obiekt TFiler dokonuje właśnie ładowania komponentu. Znacznik ten ustawiany jest w momencie rozpoczęcia tworzenia egzemplarza komponentu i ustawiony jest aż do chwili, gdy komponent zostanie załadowany wraz z komponentami, dla których jest właścicielem; jego zerowania dokonuje metoda TComponent::Loaded(), zaś ustawiany jest przez metody TReader::ReadComponent() i TReader::ReadRootComponent(). |
csReading |
Komponent dokonuje właśnie odczytu swej właściwości ze strumienia. W przeciwieństwie do znacznika csLoading, ustawionego przez cały czas ładowania komponentu, ustawiany jest tylko na czas odczytu poszczególnych właściwości. Znacznik ten modyfikowany jest przez metody TReader::ReadComponent() i TReader::ReadRootComponent(). |
csWriting |
Komponent dokonuje zapisu swej właściwości do strumienia. Znacznik ten modyfikowany jest przez metodę TWriter::WriteComponent(). |
csUpdating |
Komponent jest właśnie modyfikowany w konsekwencji zmian poczynionych w formularzu nadrzędnym (bazowym). Znacznik ten ustawiany jest tylko łącznie ze znacznikiem csAncestor; jego ustawiania dokonuje metoda TComponent::Updating(), zaś zerowania - metoda TComponent::Updated(). |
Spośród powyższych znaczników najprostszym koncepcyjnie i najczęściej wykorzystywanym przez użytkownika jest oczywiście csDesigning. Główną przesłanką rozróżniania etapów projektowania i wykonania jest oczywisty fakt, iż na etapie projektowania użytkownik sprawuje nieporównywalnie większą kontrolę na komponentami; na etapie tym komponenty są jednak z reguły odseparowane od rzeczywistych danych, z którymi przyjdzie pracować uruchomionej aplikacji. Naturalną konsekwencją tych różnic są zachowania aplikacji charakterystyczne wyłącznie dla jednego z etapów, między innymi:
weryfikacja właściwości komponentu w kontekście powiązania z innymi komponentami; na etapie wykonania weryfikacja taka nie ma zazwyczaj sensu wobec oczywistego faktu, iż pozostałe komponenty zwykle są jeszcze niekompletne;
ostrzeżenia wyświetlane w przypadku ustawienia niepoprawnej wartości właściwości w inspektorze obiektów;
podpowiedzi kontekstowe i różnego rodzaju dialogi ułatwiające użytkownikowi wybór stosownych ustawień poszczególnych właściwości na etapie projektowania.
Powiązania między komponentami
Poszczególne komponenty projektu mogą być ze sobą powiązane za pośrednictwem swych właściwości. Przykładowo obiekt TDriveComboBox, udostępniający rozwijalną listę napędów dostępnych w systemie, powiązany jest zazwyczaj z komponentem TDirectoryListBox, reprezentującym drzewo katalogów wybranego napędu; za powiązanie to odpowiedzialna jest jego właściwość DirList. Sam fakt powiązania komponentów nie jest oczywiście niczym niezwykłym, najistotniejszym zagadnieniem są bowiem konsekwencje owego powiązania, a dokładniej - właściwe reagowanie komponentu na zmiany zachodzące w komponentach z nim powiązanych. Wybierając (w czasie wykonania aplikacji) jeden z napędów w oknie komponentu TDriveComboBox, spowodujemy automatyczne uaktualnienie zawartości połączonego z nim komponentu TDirectoryListBox - bez napisania chociażby jednego wiersza kodu.
Aby zademonstrować różne aspekty powiązania komponentów, skonstruowaliśmy prosty komponent TMsgLog, którego zadaniem jest przekazywanie komunikatów do kontrolek w rodzaju TMemo lub TRichEdit, ogólnie - kontrolek wywodzących się z klasy TCustomControl. Deklarację tego komponentu przedstawia wydruk 7.30; nietrudno zauważyć, iż elementem odpowiedzialnym za powiązanie ze wspomnianymi kontrolkami jest właściwość LinkEdit, bazująca na prywatnym polu FLinkEdit.
Wydruk 7.30. Deklaracja komponentu TMsgLog
class PACKAGE TMsgLog : public TComponent
{
private:
TCustomMemo *FLinkedEdit;
public:
__fastcall TMsgLog(TComponent* Owner);
__fastcall ~TMsgLog(void);
void __fastcall OutputMsg(const AnsiString Message);
protected:
virtual void __fastcall Notification
(TComponent *AComponent, TOperation Operation);
__published:
__property TCustomMemo *LinkedEdit =
{read = FLinkedEdit, write = FLinkedEdit};
};
Właściwość LinkEdit, jako opublikowana, pojawi się w oknie inspektora obiektów; na liście komponentów, których wskazanie może stanowić jej wartość, znajdą się przy tym wyłącznie te komponenty formularza, które wywodzą się z klasy TCustomMemo - o tym przejawie „inteligencji” inspektora obiektów pisaliśmy już w rozdziale 6.
Niewątpliwie wartość nadana właściwości LinkEdit zachowuje swą aktualność tylko do chwili, w której zwolniony zostanie wskazywany przez nią obiekt; stanowi to wyzwanie pod adresem wiarygodności całego mechanizmu powiązania komponentów - zwolnienie wskazywanego komponentu nie może w żadnym wypadku pozostać niezauważone dla naszej kontrolki TMsgLog. I rzeczywiście takim nie pozostanie, bowiem w momencie wstawiania nowego komponentu na formularz lub usuwania z niego dowolnego komponentu wszystkie pozostałe komponenty zostaną powiadomione o tym fakcie poprzez wywołanie metody Notification każdego z nich. Metoda ta posiada dwa parametry: pierwszy z nich jest wskaźnikiem odnośnego komponentu, drugi zaś informuje, czy mamy do czynienia ze wstawianiem komponentu (opInsert), czy z jego usuwaniem (opRemove) - w tym ostatnim przypadku nasz komponent powinien „wyzerować” swoją właściwość LinkEdit:
Wydruk 7.31. Usuwanie połączenia z usuniętym komponentem
void __fastcall TMsgLog::Notification(TComponent *AComponent,
TOperation Operation)
{
// nie interesują nas operacje inne niż usuwanie komponentu
if(Operation != opRemove)
return ;
// sprawdź, czy usunięcie dotyczy wskazywanego komponentu
// i jeżeli tak, to wyzeruj wskazanie na niego
if(AComponent == FLinkedEdit)
FLinkedEdit = NULL;
}
Przekazaniem kolejnego komunikatu do wskazywanej kontrolki zajmuje się metoda OutputMsg() - i tu zaczyna się kolejny problem: otóż sposób tego przekazania różny będzie dla kontrolek TMemo i TRichEdit. Można by co prawda uniknąć tego kłopotu, ograniczając się tylko do tego, co oferuje klasa TCustomMemo, a więc właściwości Lines; nasz komponent wykazuje się jednak pewnym stopniem „inteligencji” - jeżeli mianowicie komponent wskazywany przez LinkEdit jest kontrolką klasy TRichEdit lub pochodnej (co łatwo sprawdzić za pomocą rzutowania dynamicznego), wykorzystana zostanie możliwość ustawienia koloru jego czcionki. Oto treść metody OutputMsg():
Wydruk 7.32. Metoda przekazująca komunikat
void __fastcall TMsgLog::OutputMsg(const AnsiString Message)
{
TMemo *LinkedMemo = 0;
TRichEdit *LinkedRichEdit = 0;
LinkedMemo = dynamic_cast<TMemo *>(FLinkedEdit);
LinkedRichEdit = dynamic_cast<TRichEdit *>(FLinkedEdit);
if (LinkedRichEdit)
{
LinkedRichEdit->Font->Color = clRed;
LinkedRichEdit->Lines->Add(Message);
}
else if (LinkedMemo)
{
LinkedMemo->Lines->Add(Message);
}
}
Zamieniłem listingi 9.31 i 9.32, bowiem musiałem przeredagować treść. Oryginalny listing 9.31 (u mnie 7.32) jest błędny i bezsensowny, więc go poprawiłem jak należy.
Przedstawiony przykład ma charakter wręcz elementarny, jednak w przypadku komponentów bardziej złożonych, gdzie powiązania są bardziej skomplikowane (jak np. wśród komponentów bazodanowych), poprawna obsługa wszelkich zależności pomiędzy powiązanymi komponentami jest sprawą pierwszorzędną. Dobrze zaprojektowany komponent poznaje się bowiem po tym, iż jego spodziewane funkcjonowanie daje się osiągnąć przy minimum wysiłku.
Nieco bardziej zaawansowanym przejawem powiązania pomiędzy komponentami jest zdolność reagowania danego komponentu na zdarzenia zachodzące w kontekście komponentów z nim powiązanych. Kontrolka wskazywana przez właściwość LinkEdit niekoniecznie musi być kontrolką tylko do odczytu; użytkownik być może będzie miał ochotę uzupełniać zawarte w niej komunikaty własnymi komentarzami. Gdy po zakończeniu ewentualnej modyfikacji „przełączy” się on na inną kontrolkę, wygenerowane zostanie zdarzenie OnExit (w kontekście kontrolki dotychczas aktywnej) i ten właśnie moment może zostać przechwycony przez nasz komponent TMsgLog. Należy w tym celu zapamiętać oryginalny adres funkcji obsługującej zdarzenie OnExit we wskazywanej kontrolce, „podpiąć” adres własnej funkcji zdarzeniowej, a na końcu treści tej ostatniej wywołać oryginalną funkcję zdarzeniową. Owa „własna” funkcja zdarzeniowa związana będzie z „własnym” zdarzeniem typu TNotifyEvent wskazywanym przez prywatne pole FOnUsersExit - należy więc nieco zmodyfikować deklarację klasy naszego komponentu (wytłuszczono nowo dodane elementy):
Wydruk 7.33. Zmodyfikowana deklaracja komponentu TMsgLog
class PACKAGE TMsgLog : public TComponent
{
private:
TCustomMemo *FLinkedEdit;
TNotifyEvent *FPrevExit;
TNotifyEvent *FOnUsersExit;
void __fastcall MsgLogOnExit(TObject *Sender);
public:
__fastcall TMsgLog(TComponent* Owner);
__fastcall ~TMsgLog(void);
void __fastcall OutputMsg(const AnsiString Message);
protected:
virtual void __fastcall Notification
(TComponent *AComponent, TOperation Operation);
virtual void __fastcall Loaded(void);
__published:
__property TCustomMemo *LinkedEdit =
{read = FLinkedEdit, write = FLinkedEdit};
};
Najlepszym miejscem przeadresowania obsługi zdarzenia OnExit jest oczywiście metoda Loaded() wywoływana po kompletnym załadowaniu komponentu:
Wydruk 7.34. Przeadresowanie obsługi zdarzenia komponentu powiązanego
void __fastcall TMsgLog::Loaded(void)
{
TComponent::Loaded();
FPrevExit = 0;
if(!ComponentState.Contains(csDesigning))
{
if(FlinkedEdit)
{
FPrevExit = FlinkedEdit->OnExit;
FlinkedEdit->OnExit = MsgLogOnExit;
}
}
}
Dla prostoty ograniczyliśmy się do etapu wykonania skompilowanej aplikacji. Metoda MsgLogOnExit() wywołuje wpierw „podpiętą” funkcję zdarzeniową komponentu TMsgLog (o ile użytkownik takową zdefiniował - zmienna FOnUsersExit musi być koniecznie wyzerowana w konstruktorze), przechodząc następnie do oryginalnej funkcji zdarzeniowej (o ile takowa istnieje):
Wydruk 7.35. Obsługa zdarzenia OnExit komponentu powiązanego
void __fastcall TMsgLog::MsgLogOnExit(TObject *Sender);
{
if (FOnUsersExit)
{
FOnUsersExit(this);
}
if (FLinkedEdit)
{
if (FPrevExit)
{
FPrevExit(FLinkedEdit);
}
}
}
Oryginalny listing 9.34 jest bezsensowny, musiałem przeredagować treść, by w ogóle miała ona sens.
Projektowanie komponentów wizualnych
Komponenty wizualne tym różnią się od swoich niewizualnych partnerów, iż posiadają określoną reprezentację graficzną, identyczną na obydwu etapach - projektowania i wykonania aplikacji. Gdy zmienia się któraś z właściwości odpowiedzialnych za tę reprezentację - czy to wskutek wykonania kodu aplikacji, czy to w konsekwencji operowania mechanizmami inspektora obiektów - reprezentacja ta musi zostać odtworzona. O ile większość kontrolek „okienkowych” stanowi prostą enkapsulację standardowych kontrolek Windows, o których wygląd należycie troszczy się sam system operacyjny, to jednak utrzymywanie właściwego wyglądu kontrolek definiowanych przez użytkownika wymagać może pewnych dodatkowych lub specyficznych działań, które powinny znaleźć odzwierciedlenie w konstrukcji metody obsługującej zdarzenie OnPaint, generowane w reakcji na otrzymanie przez kontrolkę komunikatu WM_PAINT, nakazującego jej odtworzenie swego wyglądu.
Jak już wcześniej pisaliśmy, niezmiernie istotnym zagadnieniem przy tworzeniu nowego komponentu jest właściwy wybór klasy bazowej, konieczne może się więc okazać w związku z tym przestudiowanie wielu haseł systemu pomocy czy nawet kodu źródłowego biblioteki VCL. Nie ma bowiem nic gorszego nad smutną konstatację, iż komponent opracowywany z mozołem przez wiele dni nie posiada tych możliwości, których od niego oczekiwaliśmy.
TCanvas
Obiekt klasy TCanvas jest w bibliotece VCL reprezentantem „okienkowego” kontekstu urządzenia (ang. DC - Device Context). Udostępnia on wygodne narzędzia do rysowania grafiki i skomplikowanych kształtów geometrycznych. Począwszy od klasy TCustomControl każdy komponent posiada swego rodzaju „płótno”, przedstawiające jego wygląd graficzny - płótno to ukrywa się pod właściwością Canvas i jest oczywiście obiektem klasy TCanvas.
Poniższy wydruk ilustruje wykonanie na płótnie komponentu prostej operacji graficznej - narysowanie przekątnej biegnącej z lewego górnego narożnika:
Wydruk 7.36. Rysowanie przekątnej na płótnie komponentu
Canvas->MoveTo(0, 0);
int X = ClientRect.Right;
int Y = ClientRect.Bottom;
Canvas->LineTo(X, Y);
Pierwsza z instrukcji ustawia punkt o współrzędnych (0, 0) - czyli lewy górny narożnik - jako wyróżnioną („bieżącą”) pozycję. Następnie pod zmienne X i Y podstawiane są wymiary płótna, stanowiące jednocześnie współrzędne prawego dolnego narożnika. Wreszcie metoda LineTo()rysuje odcinek linii prostej pomiędzy wyróżnioną pozycją a wskazanym punktem.
Fragment kodu z wydruku 7.37 dokonuje obramowania płótna trójwymiarową ramką, dzięki czemu komponent zyskuje wygląd podobny do przycisku:
Wydruk 7.37. Obramowanie płótna
int PenWidth = 2;
TColor Top = clBtnHighlight;
TColor Bottom = clBtnShadow;
Frame3D(Canvas, ClientRect, Top, Bottom, PenWidth);
Dzięki właściwości Canvas możliwe jest również wykorzystanie szerokiej gamy funkcji API, operujących w oparciu o kontekst urządzenia. Kontekst ten dostępny jest pod właściwością Handle płótna, może być również otrzymany za pomocą funkcji GetDC() na podstawie uchwytu kontrolki; wykonanie każdej z poniższych instrukcji daje identyczny efekt:
HDC dc = SomeComponent->Canvas->Handle;
...
HDC dc = GetDC(SomeComponent->Handle);
Ilustracją wykorzystania płótna komponentu są trzy przykładowe projekty, znajdujące się w katalogach PaintBox1, PaintBox2 i PaintBox3 na załączonej płycie CD-ROM. Używają one komponentu TPaintBox, ponieważ jego właściwość Canvas została opublikowana; jako że komponent ten w głównej mierze odpowiedzialny jest za wygląd formularza, rysowanie jego zawartości powinno stanowić część scenariusza „odrysowywania” okna w odpowiedzi na komunikat WM_PAINT, powinno więc stanowić treść funkcji obsługującej zdarzenie OnPaint.
Pierwszy z projektów ilustruje rysowanie elipsy, będącej w istocie prostokątem o przesadnie zaokrąglonych narożnikach; jego funkcję „odrysowującą” przedstawia wydruk 7.38.
Wydruk 7.38. Rysowanie elipsy na płótnie komponentu
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TRect Rect = PaintBox1->ClientRect;
int nLeftRect, nTopRect, nRightRect, nBottomRect, nWidth, nHeight;
nLeftRect = Rect.Left;
nTopRect = Rect.Top;
nRightRect = Rect.Right;
nBottomRect = Rect.Bottom;
nWidth = Rect.Right - Rect.Left + 50;
nHeight = Rect.Bottom - Rect.Top;
if(RoundRect(
PaintBox1->Canvas->Handle, // uchwyt kontekstu urządzenia
nLeftRect, // współrzędne prostokąta
nTopRect, // ...
nRightRect, // ...
nBottomRect, // ...
nWidth, // osie elipsy określającej zaokrąglenie narożników
nHeight // ...
) == 0)
ShowMessage("Rysowanie nie udało się ...");
}
Pouczającym doświadczeniem może być zaobserwowanie skutków zmian wartości deklarowanych zmiennych - zarówno tych określających współrzędne prostokąta (nLeftRect, nTopRect, nRightRect, nBottomRect), jak i tych decydujących o wyglądzie zaokrąglonych narożników (nWidth, nHeight); zerowa wartość tych ostatnich oznacza brak zaokrąglenia.
Wykorzystanie kontrolek graficznych
Do wielu czynników decydujących o atrakcyjności aplikacji dołączył w ostatnim dziesięcioleciu również ich wygląd graficzny. Rozmaite programy komercyjne, shareware i freeware prześcigają się w rozmaitych pomysłach graficznych, a powszechnie używane kontrolki, zadowalające się niegdyś wyłącznie opisem tekstowym, wypierane są konsekwentnie przez swe odpowiedniki graficzne - czego przykładem chociażby przyciski TSpeedButton i TBitBtn. Wychodząc naprzeciw tym realiom, narzędzia do wizualnego projektowania aplikacji, jak Delphi i C++Builder, oferują szereg klas ułatwiających zarządzanie bitmapami, ikonami, obrazkami typu JPEG, GIF itp.
Kolejny z naszych przykładów ilustruje wszechobecną już w aplikacjach manierę symulowania trójwymiarowego wyglądu kontrolek. Sugestywne wrażenie wypukłości lub wklęsłości prostokąta powodowane jest w rzeczywistości odpowiednio dobranymi odcieniami ramki otaczającej ów prostokąt, o czym można się łatwo przekonać, studiując kod źródłowy projektu. Bieżący stan prostokąta (wypukłość - wklęsłość) określony jest przez prywatną zmienną IsUp; zmienna ta przełączana jest przy każdym kliknięciu przycisku, który przy okazji dostosowuje swój tytuł do bieżącej sytuacji:
Tu proszę wkleić rysunek AG-9-1.BMP.
Rysunek 7.1. Symulowanie trójwymiarowego wyglądu kontrolki
Wydruk 7.39. Przełączanie stanu prostokąta
void __fastcall TForm1::Button1Click(TObject *Sender)
{
IsUp = !IsUp;
Button1->Caption = (IsUp) ? "Wklęsły" : "Wypukły";
PaintBox1->Repaint();
}
Ostatnia z instrukcji wywołuje metodę odrysowującą zawartość prostokąta:
Wydruk 7.40. Rysowanie prostokąta
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TColor TopColor, BottomColor;
TRect Rect;
Rect = PaintBox1->ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
bit->Width = PaintBox1->Width;
bit->Height = PaintBox1->Height;
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(Rect);
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);
PaintBox1->Canvas->Draw(0, 0, bit);
delete bit;
}
Spoglądając na powyższy wydruk, nietrudno zauważyć jeszcze jedną tendencję typową dla aplikacji graficznych. Mianowicie, aby zminimalizować migotanie poszczególnych elementów graficznych, opatrująca je grafika tworzona jest najpierw na roboczej, niewidocznej dla użytkownika bitmapie (w powyższym wydruku bitmapę taką wskazuje zmienna bit), a następnie w szybki sposób kopiowana na płótno komponentu docelowego (w powyższym wydruku czyni to przedostatnia z instrukcji).
Kolory ramki otaczającej prostokąt, rysowanej za pomocą funkcji API Frame3D(), określane są za pomocą metody SwapColors(), która po prostu zapożycza je ze standardowego przycisku i ustawia we właściwej kolejności stosownie do wartości zmiennej IsUp:
Wydruk 7.40. Określanie kolorów ramki
void __fastcall TForm1::SwapColors(TColor &Top, TColor &Bottom)
{
Top = (IsUp) ? clBtnHighlight : clBtnShadow;
Bottom = (IsUp) ? clBtnShadow : clBtnHighlight;
}
W rzeczywistości wygląd kontrolek graficznych w „prawdziwych” aplikacjach jest nieco bardziej złożony, zamiast bowiem zwykłych linii czy elips mamy tu do czynienia z kompletnymi bitmapami, zazwyczaj przechowywanymi w odrębnych plikach lub zasobach (patrz rys. 7.2). Komplikuje to ponownie czynność utrzymywania właściwego wyglądu kontrolek: zawartość wspomnianych plików (zasobów) wczytywana jest do osobnych roboczych bitmap, te ostatnie wkopiowywane są na „zbiorczą” bitmapę roboczą, która dopiero kopiowana jest na płótno komponentu. Ilustruje to trzeci z naszych projektów demonstracyjnych - funkcję obsługującą zdarzenie OnPaint użytego w nim komponentu TPaintBox przedstawia wydruk 7.41.
Tu proszę wkleić rysunek AG-9-2.BMP
Rysunek 7.2. Wzbogacenie wyglądu kontrolki za pomocą zewnętrznej bitmapy
Wydruk 7.41. Wykorzystanie bitmap zewnętrznych
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
TColor TopColor, BottomColor;
TRect Rect, gRect;
Rect = PaintBox1->ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
Graphics::TBitmap *bitFile = new Graphics::TBitmap;
bitFile->LoadFromFile("geom1b.bmp");
// rozmiar roboczej bitmapy równy jest rozmiarowi płótna komponentu
bit->Width = PaintBox1->Width;
bit->Height = PaintBox1->Height;
// wypełnij płótno bitmapy domyślnym kolorem pędzla
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(Rect);
// wycentruj zewnętrzną bitmapę (w poziomie i w pionie)
// na płótnie komponentu
gRect.Left = ((Rect.Right - Rect.Left) / 2) - (bitFile->Width / 2);
gRect.Top = ((Rect.Bottom - Rect.Top) / 2) - (bitFile->Height / 2);
// jeśli wklęsłość, to przesuń wewnętrzny prostokąt o 1 piksel
// w prawo i w dół
gRect.Top += (IsUp) ? 0 : 1;
gRect.Left += (IsUp) ? 0 : 1;
gRect.Right = bitFile->Width + gRect.Left;;
gRect.Bottom = bitFile->Height + gRect.Top;
// kopiuj na bitmapę zbiorczą, respektując kolor przezroczysty
bit->Canvas->BrushCopy(gRect, bitFile,
TRect(0,0,bitFile->Width, bitFile->Height), bitFile->TransparentColor);
// narysuj obrzeże
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2);
// kopiuj zbiorczą bitmapę na płótno komponentu
BitBlt(PaintBox1->Canvas->Handle, 0, 0, PaintBox1->ClientWidth,
PaintBox1->ClientHeight, bit->Canvas->Handle, 0, 0, SRCCOPY);
delete bitFile; // usuń bitmapę wczytaną z pliku
delete bit; // usuń bitmapę zbiorczą
}
Reagowanie na zdarzenia pochodzące od myszy
Kontrolki graficzne konstruowane są zazwyczaj na bazie klasy TGraphicControl, nie są więc kontrolkami okienkowymi, i nie posiadając uchwytu (Handle), nie mogą przyjmować skupienia. Mimo to biblioteka VCL udostępnia mechanizmy umożliwiające reagowanie kontrolkom graficznym na zdarzenia pochodzące od myszy.
Dobrze znany wszystkim projektantom przycisk TSpeedButton, gdy ustawić na true jego właściwość Flat, pozostaje wkomponowany w tło, nie ukazując swego obrzeża; obrzeże to staje się jednak natychmiast widoczne, gdy zatrzymać nad rzeczonym przyciskiem kursor myszy (rys. 7.3). Odpowiedzialnymi za ten efekt są dwa komunikaty Windows - CM_MOUSEENTER i CM_MOUSELEAVE. Innym równie użytecznym komunikatem jest CM_ENABLEDCHANGED, zmieniający status „dostępności” kontrolki; jest on wysyłany do kontrolki każdorazowo, gdy zmienia się jej właściwość Enabled:
procedure TControl.SetEnabled(Value: Boolean);
begin
if FEnabled <> Value then
begin
FEnabled := Value;
Perform(CM_ENABLEDCHANGED, 0, 0);
end;
end;
Tu proszę wkleić rysunek AG-9-3.BMP
Rysunek 7.3. „Płaski” przycisk na formularzu
Wymienione komunikaty obsługiwane są przez dedykowane metody komponentu, co wynika z deklaracji w jego pliku nagłówkowym:
Wydruk 7.42. Deklaracja metod obsługujących komunikaty CM_MOUSEENTER, CM_MOUSELEAVE i CM_ENABLEDCHANGED
...
MESSAGE void __fastcall CMMouseEnter(TMessage &Msg);
MESSAGE void __fastcall CMMouseLeave(TMessage &Msg);
MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg);
...
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)
MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)
MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)
END_MESSAGE_MAP(TGraphicControl)
Typowe zdarzenia związane z myszą - OnMouseUp, OnMouseDown, OnMouseOver itp. - obsługiwane są przez metody klasy TControl zdefiniowane w jej sekcji protected. Można je oczywiście przedefiniować, nie zapominając, by ich deklaracje pozostały w sekcji protected. Ilustruje to wydruk 7.43.
Wydruk 7.43. Przedefiniowanie standardowej obsługi zdarzeń myszy
....
private:
TMouseEvent FOnMouseUp;
TMouseEvent FOnMouseDown;
TMouseMoveEvent FOnMouseMove;
protected:
DYNAMIC void __fastcall MouseDown
(TMouseButton Button, TShiftState Shift, int X, int Y);
DYNAMIC void __fastcall MouseMove
(TShiftState Shift, int X, int Y);
DYNAMIC void __fastcall MouseUp
(TMouseButton Button, TShiftState Shift, int X, int Y);
__published:
__property TMouseEvent OnMouseUp = {read=FOnMouseUp, write=FOnMouseUp};
__property TMouseEvent OnMouseDown =
{read=FOnMouseDown, write=FOnMouseDown};
__property TMouseMoveEvent OnMouseMove =
{read=FOnMouseMove, write=FOnMouseMove};
....
Przykład zastosowania
Ilustracją przedstawionych informacji jest przykładowy komponent TExampleButton, którego moduł źródłowy znajduje się na załączonej płycie CD-ROM (pliki ExampleButton.h i ExampleButton.cpp). Jest on daleki od kompletności i jako taki stanowić może wdzięczny materiał do eksperymentowania, niemniej jednak i tak w obecnej postaci nadaje się do zainstalowania w palecie komponentów. Treść wspomnianych plików źródłowych prezentujemy na dwóch poniższych wydrukach.
Wydruk 7.44. Plik nagłówkowy ExampleButton.h
// TExampleButton
// By: Sean Rock, rev. by A.Grażyński
//---------------------------------------------------------------------------
#ifndef ExampleButtonH
#define ExampleButtonH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
//---------------------------------------------------------------------------
enum TExButtonState {esUp, esDown, esFlat, esDisabled};
class PACKAGE TExampleButton : public TGraphicControl
{
private:
Graphics::TBitmap *FGlyph;
AnsiString FCaption;
TImageList *FImage;
TExButtonState FState;
bool FMouseInControl;
TNotifyEvent FOnClick;
void __fastcall SetGlyph(Graphics::TBitmap *Value);
void __fastcall SetCaption(AnsiString Value);
void __fastcall BeforeDestruction(void);
void __fastcall SwapColors(TColor &Top, TColor &Bottom);
void __fastcall CalcGlyphLayout(TRect &r);
void __fastcall CalcTextLayout(TRect &r);
MESSAGE void __fastcall CMMouseEnter(TMessage &Msg);
MESSAGE void __fastcall CMMouseLeave(TMessage &Msg);
MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg);
protected:
void __fastcall Paint(void);
DYNAMIC void __fastcall MouseDown(TMouseButton Button, TShiftState Shift,
int X, int Y);
DYNAMIC void __fastcall MouseUp(TMouseButton Button, TShiftState Shift,
int X, int Y);
public:
__fastcall TExampleButton(TComponent* Owner);
__published:
__property AnsiString Caption = {read=FCaption, write=SetCaption};
__property Graphics::TBitmap * Glyph = {read=FGlyph, write=SetGlyph};
__property TNotifyEvent OnClick = {read=FOnClick, write=FOnClick};
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)
MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)
MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged)
END_MESSAGE_MAP(TGraphicControl)
};
//---------------------------------------------------------------------------
#endif
Wydruk 7.45. Plik ExampleButton.cpp
// ExampleButton.cpp
// By: Sean Rock, rev. by A.Grażyński
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "ExampleButton.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TExampleButton *)
{
new TExampleButton(NULL);
}
//---------------------------------------------------------------------------
__fastcall TExampleButton::TExampleButton(TComponent* Owner)
: TGraphicControl(Owner)
{
SetBounds(0,0,50,50);
ControlStyle = ControlStyle << csReplicatable;
FState = esFlat;
}
//---------------------------------------------------------------------------
namespace Examplebutton
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TExampleButton)};
RegisterComponents("Samples", classes, 0);
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMMouseEnter(TMessage &Msg)
{
if(Enabled)
{
FState = esUp;
FMouseInControl = true;
Invalidate();
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMMouseLeave(TMessage &Msg)
{
if(Enabled)
{
FState = esFlat;
FMouseInControl = false;
Invalidate();
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CMEnabledChanged(TMessage &Msg)
{
FState = (Enabled) ? esFlat : esDisabled;
Invalidate();
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::MouseDown(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esDown;
Invalidate();
}
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esUp;
Invalidate();
if(FOnClick)
FOnClick(this);
}
}
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SetGlyph(Graphics::TBitmap * Value)
{
if(Value == NULL)
return;
if(!FGlyph)
FGlyph = new Graphics::TBitmap;
FGlyph->Assign(Value);
Invalidate();
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SetCaption(AnsiString Value)
{
FCaption = Value;
Invalidate();
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::SwapColors(TColor &Top, TColor &Bottom)
{
if(ComponentState.Contains(csDesigning))
{
FState = esUp;
}
Top = (FState == esUp) ? clBtnHighlight : clBtnShadow;
Bottom = (FState == esDown) ? clBtnHighlight : clBtnShadow;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::BeforeDestruction(void)
{
if(FImage)
delete FImage;
if(FGlyph)
delete FGlyph;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::Paint(void)
{
TRect cRect, tRect, gRect;
TColor TopColor, BottomColor;
Canvas->Brush->Color = clBtnFace;
Canvas->FillRect(ClientRect);
cRect = ClientRect;
Graphics::TBitmap *bit = new Graphics::TBitmap;
bit->Width = ClientWidth;
bit->Height = ClientHeight;
bit->Canvas->Brush->Color = clBtnFace;
bit->Canvas->FillRect(cRect);
if(FGlyph)
if(!FGlyph->Empty)
{
CalcGlyphLayout(gRect);
bit->Canvas->BrushCopy(gRect, FGlyph,
Rect(0,0,FGlyph->Width,FGlyph->Height), FGlyph->TransparentColor);
}
if(!FCaption.IsEmpty())
{
CalcTextLayout(tRect);
bit->Canvas->TextRect(tRect, tRect.Left,tRect.Top, FCaption);
}
if(FState == esUp || FState == esDown)
{
SwapColors(TopColor, BottomColor);
Frame3D(bit->Canvas, cRect, TopColor, BottomColor, 1);
}
BitBlt(Canvas->Handle, 0, 0, ClientWidth, ClientHeight,
bit->Canvas->Handle, 0, 0, SRCCOPY);
delete bit;
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CalcGlyphLayout(TRect &r)
{
int TotalHeight=0;
int TextHeight=0;
if(!FCaption.IsEmpty())
TextHeight = Canvas->TextHeight(FCaption);
// poniższa wielkość marginesu, ustalona arbitralnie na 5 pikseli,
// powinna być przedmiotem odrębnej właściwości
TotalHeight = FGlyph->Height + TextHeight + 5;
r = Rect((ClientWidth/2)-(FGlyph->Width/2),
((ClientHeight/2)-(TotalHeight/2)), FGlyph->Width +
(ClientWidth/2)-(FGlyph->Width/2), FGlyph->Height +
((ClientHeight/2)-(TotalHeight/2)));
}
// ---------------------------------------------------------------------------
void __fastcall TExampleButton::CalcTextLayout(TRect &r)
{
int TotalHeight=0;
int TextHeight=0;
int TextWidth=0;
TRect temp;
if(FGlyph)
TotalHeight = FGlyph->Height;
TextHeight = Canvas->TextHeight(FCaption);
TextWidth = Canvas->TextWidth(FCaption);
TotalHeight += TextHeight + 5;
temp.Left = 0;
temp.Top = (ClientHeight/2)-(TotalHeight/2);
temp.Bottom = temp.Top + TotalHeight;
temp.Right = ClientWidth;
r = Rect(((ClientWidth/2) - (TextWidth/2)), temp.Bottom-TextHeight,
((ClientWidth/2)-(TextWidth/2))+TextWidth, temp.Bottom);
}
Wśród opublikowanych zdarzeń znajduje się tu jedynie OnClick, chociaż w rzeczywistym komponencie listę tę należałoby raczej poszerzyć o zdarzenia OnMouseUp, OnMouseDown i OnMouseMove. Podobnie ma się sprawa z innymi właściwościami - do opublikowanych Caption i Glyph prawdopodobnie powinna dołączyć także właściwość Font, reprezentująca czcionkę, którą wypisany zostanie tytuł przycisku. Użyteczne może się również okazać przechwycenie komunikatu CM_FONTCHANGED w celu dostosowania wzajemnego położenia tytułu i obrazka określonego przez właściwość Glyph. Obecnie przyjęto sztywną, pięciopikselową odległość pomiędzy nimi - tę sztywną wartość bardziej elegancko będzie jednak powierzyć odrębnej właściwości.
Zwróćmy także uwagę na metodę SetGlyph(), będącą metodą dostępową właściwości Glyph. Łatwo zauważyć, iż metoda ta nieczuła jest na przekazanie wartości NULL, co powoduje, iż raz przypisanego komponentowi obrazka nie sposób już (na etapie projektowania) zlikwidować - można to zrobić jedynie poprzez usunięcie komponentu z formularza i zastąpienie go nowym, pobranym z palety.
Opisywany komponent (TExampleButton) ma pewną usterkę, przejawiającą się również i w niektórych innych komponentach - przypisanie właściwości Glyph jakiejś bitmapy (a więc zmiana jej domyślnej wartości NULL) powoduje przy uruchomieniu aplikacji generowanie wyjątku związanego z niemożnością odczytu tej właściwości ze strumienia.
Ostatnim elementem wartym krótkiego komentarza jest boolowska zmienna FMouseInControl. Zmienna ta używana jest do określenia, czy kursor myszy znajduje się aktualnie nad kontrolką; za utrzymywanie jej zgodności ze stanem faktycznym odpowiedzialne są metody CMMouseEnter() i CMMouseLeave(). Znajomość położenia kursora jest o tyle istotna, iż w pewnych sytuacjach kontrolka może wciąż reagować na zdarzenia myszy, mimo iż kursor znajduje się już poza nią - będzie tak np. w przypadku umieszczenia kursora nad kontrolką, a następnie przesunięcie go poza kontrolkę przy naciśniętym i trzymanym lewym przycisku. Puszczenie przycisku spowoduje wywołanie metody MouseUp - jeżeli wartość zmiennej FMouseInControl równa będzie false, metoda ta nie wykona jednak żadnej akcji:
void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState
Shift, int X, int Y)
{
if(Button == mbLeft)
{
if(Enabled && FMouseInControl)
{
FState = esUp;
Invalidate();
if(FOnClick)
FOnClick(this);
}
}
}
Obrzeże przycisku - jeżeli oczywiście jest widoczne - rysowane jest za pomocą funkcji Frame3D(). Można w tym celu użyć nieco bardziej zaawansowanej funkcji API o nazwie DrawButtonFace() - jej deklaracja, znajdująca się w pliku nagłówkowym buttons.hpp, jest następująca:
Wydruk 7.46. Deklaracja funkcji DrawButtonFace()
TRect __fastcall DrawButtonFace
(Graphics::TCanvas* Canvas, const Windows::TRect &Client,
int BevelWidth, TButtonStyle Style,
bool IsRounded, bool IsDown, bool IsFocused);
Funkcja ta rysuje „czoło” przycisku o wielkości określonej przez parametr Client na płótnie wskazanym przez parametr Canvas. Niektóre parametry istotne są tylko w pewnych okolicznościach - na przykład parametry BevelWidth, IsRounded i IsFocused mają znaczenie tylko wtedy, gdy styl przycisku (Style) równy jest bsWin31. Kod źródłowy funkcji (w Object Pascalu) prezentujemy na wydruku 7.47 - znajduje się on w pliku buttons.pas:
Wydruk 7.47. Implementacja funkcji DrawButtonFace()
function DrawButtonFace(Canvas: TCanvas; const Client: TRect;
BevelWidth: Integer; Style: TButtonStyle; IsRounded, IsDown,
IsFocused: Boolean): TRect;
var
NewStyle: Boolean;
R: TRect;
DC: THandle;
begin
NewStyle :=
((Style = bsAutoDetect) and NewStyleControls) or (Style = bsNew);
R := Client;
with Canvas do
begin
if NewStyle then
begin
Brush.Color := clBtnFace;
Brush.Style := bsSolid;
DC := Canvas.Handle;
if IsDown then
begin { DrawEdge jest szybsza niż Polyline }
DrawEdge(DC, R, BDR_SUNKENINNER, BF_TOPLEFT); { czarny }
DrawEdge(DC, R, BDR_SUNKENOUTER, BF_BOTTOMRIGHT); { btnhilite }
Dec(R.Bottom);
Dec(R.Right);
Inc(R.Top);
Inc(R.Left);
DrawEdge(DC, R, BDR_SUNKENOUTER, BF_TOPLEFT or BF_MIDDLE); {btnshadow}
end
else
begin
DrawEdge(DC, R, BDR_RAISEDOUTER, BF_BOTTOMRIGHT); { czarny }
Dec(R.Bottom);
Dec(R.Right);
DrawEdge(DC, R, BDR_RAISEDINNER, BF_TOPLEFT); {btnhilite}
Inc(R.Top);
Inc(R.Left);
DrawEdge(DC, R, BDR_RAISEDINNER, BF_BOTTOMRIGHT or BF_MIDDLE);
{btnshadow}
end;
end
else
begin
Pen.Color := clWindowFrame;
Brush.Color := clBtnFace;
Brush.Style := bsSolid;
Rectangle(R.Left, R.Top, R.Right, R.Bottom);
{zaokrąglenie narożników - dotyczy tylko przycisków w stylu Windows 3.1}
if IsRounded then
begin
Pixels[R.Left, R.Top] := clBtnFace;
Pixels[R.Left, R.Bottom - 1] := clBtnFace;
Pixels[R.Right - 1, R.Top] := clBtnFace;
Pixels[R.Right - 1, R.Bottom - 1] := clBtnFace;
end;
if IsFocused then
begin
InflateRect(R, -1, -1);
Brush.Style := bsClear;
Rectangle(R.Left, R.Top, R.Right, R.Bottom);
end;
InflateRect(R, -1, -1);
if not IsDown then
Frame3D(Canvas, R, clBtnHighlight, clBtnShadow, BevelWidth)
else
begin
Pen.Color := clBtnShadow;
PolyLine([Point(R.Left, R.Bottom - 1), Point(R.Left, R.Top),
Point(R.Right, R.Top)]);
end;
end;
end;
Result := Rect(Client.Left + 1, Client.Top + 1,
Client.Right - 2, Client.Bottom - 2);
if IsDown then OffsetRect(Result, 1, 1);
end;
Rozbudowa kontrolek okienkowych
Komponenty okienkowe - te wywodzące się z klasy TWinControl - stanowią reprezentację standardowych kontrolek Windows na gruncie biblioteki VCL. Budowanie na ich bazie komponentów pochodnych ma na celu nie tyle zmianę ich wyglądu (jak w przypadku komponentów „graficznych”), lecz raczej wzbogacanie i modyfikację ich standardowego zachowania; wiele aspektów owego zachowania określonych jest przez chronione (protected) metody komponentów.
W tym podrozdziale zaprezentujemy rozbudowę komponentu TFileListBox umieszczonego w palecie na stronie Win31 i przedstawiającego listę nazw plików danego katalogu. Do spełnianych obecnie przez niego funkcji dodamy następujące elementy:
przypisanie każdemu plikowi odpowiedniej ikony, zgodnie ze standardami Windows;
uruchomienie pliku lub otwarcie dokumentu w rezultacie dwukrotnego kliknięcia reprezentującej go pozycji;
umożliwienie bardziej selektywnego doboru zestawu plików wyświetlanych w liście;
wybranie elementu listy w wyniku kliknięcia prawym przyciskiem;
przewijanie listy w poziomie, jeżeli któryś z elementów nie mieści się w niej w całości.
Zachowamy jednocześnie bardzo istotną cechę oryginalnego komponentu, mianowicie
połączenie z komponentem TDirectoryListBox za pośrednictwem jego właściwości FileList.
Rysunek 7.4 ukazuje różnicę pomiędzy obydwiema kontrolkami - oryginalną (TFileListBox) i rozbudowaną (TSHFileListBox) - ukazującymi ten sam fragment katalogu.
Rysunek 7.4. Kontrolki TFileListBox i TSHFileListBox
Powyższy rysunek znajduje się w pliku AG-9-4.BMP. Otoczyłem rysunek separatorami strony tylko po to, by było wiadome, gdzie mają wskazywać strzałki.
Na początku należy się oczywiście zastanowić nad wyborem klasy bazowej naszego komponentu. Biblioteka VCL zawiera szeroką gamę komponentów o nazwach rozpoczynających się od TCustom..., które to komponenty przeznaczone są właśnie do pełnienia „materiału wyjściowego” dla nowo tworzonych „prawdziwych” komponentów. Bezpośrednim przodkiem komponentu TFileListBox jest TCustomListBox, tego ostatniego niestety nie da się użyć na nasze potrzeby ze względu na postulat wyrażony w ostatnim punkcie prezentowanej przed chwilą listy - właściwość TDirectoryListBox:: FileList stanowi bowiem wskazanie na komponent typu TFileListBox, a więc inspektor obiektów nie umożliwiłby skojarzenia z nią komponentu wyprowadzonego z innej klasy; zmuszeni jesteśmy więc pozostać przy klasie TFileListBox jako klasie bazowej.
Skoro wybraliśmy już klasę bazową, przystąpmy do jej uzupełniania o niezbędne właściwości, metody i zdarzenia. Rozpoczniemy od „uruchamiania” wybranego elementu lub skojarzonej z nim aplikacji w wyniku dwukrotnego kliknięcia. Dwukrotne kliknięcie w obszarze listy powoduje najpierw wybranie („podświetlenie”) elementu znajdującego się aktualnie pod kursorem, a następnie wygenerowanie wywołanie metody DblClick listy jako całości.
Metoda ta zdefiniowana jest w klasie TControl w postaci niżej podanej i w klasie TFileListBox pozostaje niezmieniona:
procedure TControl.DblClick;
begin
if Assigned(FOnDblClick) then FOnDblClick(Self);
end;
Nasz komponent rozszerzony zostanie o możliwość uprzedniego „uruchomienia” wybranego elementu (dokładniej - wszystkich wybranych elementów, bowiem lista umożliwiać może wybór wielokrotny) - akcja ta wykonywana będzie zależnie od wartości (prywatnej) zmiennej boolowskiej FCanLaunch. Oto treść zmodyfikowanej funkcji, obsługującej zdarzenie dwukrotnego kliknięcia:
Wydruk 7.48. Obsługa zdarzenia OnDblClick
void __fastcall TSHFileListBox::DblClick(void)
{
if(FCanLaunch) // czy skojarzenie aktywne?
{
int ii=0;
// obsłuż podświetlone elementy listy
for(ii=0; ii < Items->Count; ii++)
{
if(Selected[ii])
{
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);
}
}
}
// wywołaj funkcję obsługi zdarzenia OnDblCLick
if(FOnDblClick)
FOnDblClick(this);
}
Uruchomienia aplikacji skojarzonej z danym elementem listy (lub aplikacji przez ten element reprezentowanej) dokonuje funkcja API o nazwie ShellExecute(). Pierwszy parametr jej wywołania jest uchwytem macierzystego okna aplikacji (tu: formularza głównego), drugi określa rodzaj akcji do wykonania (tu: otwarcie), trzeci natomiast jest łańcuchem ASCIIZ, zawierającym nazwę pliku.
Po obsłużeniu wszystkich „podświetlonych” elementów wywołana zostaje ta funkcja, którą w inspektorze obiektów przyporządkowano zdarzeniu OnDblClick; właściwość reprezentująca to zdarzenie bazuje bowiem na prywatnym polu FOnDblClick
__property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick}
wykorzystywanym w końcówce cytowanej funkcji.
Niestety, to wszystko to tylko teoria. bo autor przeoczył bardzo istotną okoliczność: jeżeli plik reprezentowany przez daną pozycję należy do zarejestrowanych typów plików, nie jest wyświetlane jego rozszerzenie, a po dwukrotnym jego kliknięciu do funkcji ShellExecute()przekazywana jest nazwa bez rozszerzenia i cała idea bierze w łeb, bo funkcja ta wybiera skojarzoną aplikację właśnie na podstawie rozszerzenia. . Niestety, ja nie potrafię.
AG
Kolejne rozszerzenie naszego komponentu to wybranie pozycji listy w wyniku kliknięcia prawym przyciskiem. Funkcja ta uzależniona jest od właściwości RightBtnClick, bazującej na prywatnym polu FRightBtnSelect
__property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel,
default=true};
zaś jej zaimplementowanie opiera się na przechwyceniu zdarzenia OnMouseUp generowanego w momencie puszczenia prawego przycisku:
Wydruk 7.49. Obsługa zdarzenia OnMouseUp
void __fastcall TSHFileListBox::MouseUp(TMouseButton Button,
TShiftState Shift, int X, int Y)
{
// interesuje nas tylko prawy klawisz
if(!FRightBtnSel)
return;
TPoint ItemPos = Point(X,Y);
// czy pod kursorem znajduje się konkretna pozycja ?
int Index = ItemAtPos(ItemPos, true);
// nie, nie zaznaczaj żadnej pozycji
if(Index == -1)
return;
// zaznacz pozycję pod kursorem
Perform(LB_SETCURSEL, (WPARAM)Index, 0);
}
W momencie puszczenia prawego klawisza sprawdza się, czy kursor myszy znajduje się nad którąś z pozycji listy - metoda ItemAtPos() próbuje skojarzyć punkt o wskazanych współrzędnych (tu: wierzchołek kursora) z konkretną pozycją listy, zwracając indeks tej pozycji; jeżeli skojarzenie takie nie jest możliwe (we wskazanym miejscu nie ma żadnej pozycji), metoda zwraca wartość -1. W przypadku udanego skojarzenia symulowane jest wystąpienie komunikatu LB_SETCURSEL, nakazującego zaznaczenie pozycji o wskazanym indeksie.
Niestety, to również nie jest dokładnie tak: zaznaczanie prawym klawiszem nie działa, jeżeli ustawiony jest tryb wielokrotnego zaznaczania (MultiSelect=true).
Komponent TFileListBox niekoniecznie musi ukazywać wszystkie pliki danego katalogu - za pomocą właściwości Mask można mianowicie ograniczyć zestaw widocznych plików tylko do tych, których nazwa zgodna jest z wyspecyfikowaną maską (w zwyczajowym rozumieniu znaków blankietowych „*” oraz „?”). Proces „filtrowania” plików można jednak uczynić bardziej selektywnym - i to właśnie jest istotą kolejnego rozszerzenia. Każdorazowo, gdy nazwa pliku - po stwierdzeniu jej zgodności z maską - ma zostać dodana do listy, generowane jest zdarzenie OnAddItem o typie określonym następująco:
typedef void __fastcall (__closure *TAddItemEvent)
(TObject *Sender, AnsiString Item, bool &CanAdd);
Pierwszy parametr (Sender) stanowi oczywiście wskazanie na listę, drugi (Item) jest łańcuchem podlegającym kwalifikowaniu, trzeci natomiast określa wynik tego kwalifikowania - jeżeli jego domyślna wartość true zostanie (w ramach funkcji obsługi) zamieniona na false, badana pozycja nie zostanie dodana do listy.
Kolejna usterka komponentu polega na tym, iż w przypadku zmiany domyślnej wartości maski *.* w uruchomionej aplikacji generowany jest wyjątek w związku z niemożnością odczytu tej właściwości ze strumienia.
Następne rozszerzenie naszego komponentu polega na opatrzeniu wyświetlanych nazw plików stosownymi ikonami - na takiej samej zasadzie, jak czyni to Eksplorator Windows. Oto najistotniejszy fragment kodu związany z tym procesem:
Wydruk 7.50. Odczytanie listy ikon odpowiadających poszczególnym plikom
void __fastcall TSHFileListBox::GetSysImages(void)
{
SHFILEINFO shfi;
DWORD iHnd;
if(!FImages)
{
FImages = new TImageList(this);
FImages->ShareImages = true;
FImages->Height = 16;
FImages->Width = 16;
iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX |
SHGFI_SHELLICONSIZE | SHGFI_SMALLICON);
if(iHnd != 0)
FImages->Handle = iHnd;
}
}
Funkcja SHGetFileInfo() dokonuje odczytu niezbędnej informacji systemowej związanej z ikonami plików. Struktura danych niosąca tę informację, identyfikowana przez uchwyt zwracany przez funkcję SHGetFileInfo(), reprezentowana jest na gruncie VCL przez obiekt TImageList, zaś wspomniany uchwyt ukrywa się pod jego właściwością Handle. Z właściwością tą związana jest inna istotna właściwość - ShareImages: ustawienie jej na true powoduje, iż obiekt TImageList podczas swej destrukcji nie zwalnia uchwytu kryjącego się pod właściwością Handle - analizując wydruk 7.50 nietrudno zauważyć, iż uchwyt ten utworzony został niezależnie od tegoż obiektu, stanowiącego tylko swego rodzaju „obudowę”. Generalnie rzecz biorąc, właścicielem uchwytu zwracanego przez SHGetFileInfo() jest system operacyjny - zwolnienie tegoż uchwytu spowodowałoby nieodwracalną (aż do ponownego załadowania systemu) utratę wszelkich ikon opatrujących obiekty Eksploratora, opcje menu itp.
Szczegółowy opis funkcji SHGetFileInfo() znajduje się w systemie pomocy Windows SDK.
Metoda AddItem(), dodająca do wyświetlanej listy kolejnej nazwy wraz z odpowiadającą jej ikoną, skonstruowana została w oparciu o interfejs COM (ang. Component Object Model); jej kod źródłowy przedstawia wydruk 7.51, lecz szczegółowa analiza użytych tu mechanizmów wykracza poza ramy tego rozdziału. Element reprezentujący systemową strukturę, zawierającą informację o dodawanej nazwie pliku, przekazywany jest jako jedyny parametr wywołania (definicja typu LPITEMIDLIST dostępna jest w systemie pomocy „Windows SDK” pod hasłem „Item Identifiers and Identifier Lists”), zaś wynikiem funkcji jest szerokość (w pikselach) tekstu wypisującego tę nazwę (jeżeli nazwa nie zostanie dodana do listy, funkcja zwraca wartość 0).
Wydruk 7.51. Dodawanie kolejnej nazwy do listy
int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)
{
SHFILEINFO shfi;
int Index;
SHGetFileInfo(
(char*)pidl,
0,
&shfi,
sizeof(shfi),
SHGFI_PIDL | SHGFI_SYSICONINDEX | SHGFI_SMALLICON |
SHGFI_DISPLAYNAME | SHGFI_USEFILEATTRIBUTES
);
// zweryfikuj nazwę pod kątem dodania jej do listy,
// generując zdarzenie OnAddItem
bool FCanAdd = true;
if(FOnAddItem)
FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);
if(FCanAdd)
{
TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);
Index = Items->AddObject(AnsiString(shfi.szDisplayName),
(TObject*)ShellInfo);
// zwróć szerokość tekstu reprezentującego nazwę
return Canvas->TextWidth(Items->Strings[Index]);
}
// zwróć zero jako sygnał, iż nazwa nie została dodana do listy
return 0;
}
Wytłuszczony fragment to opisana przed chwilą weryfikacja nazwy za pomocą generowanego zdarzenia OnAddItem. Jeżeli nazwa zakwalifikowana zostanie do dodania jej do listy, wraz z nią dodawany jest do tejże listy (jako element właściwości Objects[]) obiekt klasy TShellFileListItem, zawierający m.in. indeks ikony (w liście utworzonej na wydruku 7.50) odpowiadającej tej nazwie; umożliwi to późniejsze opatrywanie wypisywanych nazw stosownymi ikonami.
Obiekt TShellFileListItem zawiera także kopię parametru wywołania metody AddItem(). Kopia ta może zostać wykorzystana do przyszłych rozszerzeń komponentu, np. dla wyświetlenia menu kontekstowego związanego z daną nazwą.
Może właśnie za pomocą wspomnianej kopii uda się naprawić usterkę nr 2; kopia ta zawiera prawdopodobnie pełną nazwę wraz z rozszerzeniem, co można wnioskować po takim fragmencie wydruku 9.51 (w oryginale - str 374):
SHGetFileInfo(
(char*)pidl,
.....
);
i wobec tego być może w metodzie DblCLick() fragment:
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(),
0,0,SW_SHOWDEFAULT);
można by zastąpić takim fragmentem:
TShellFileListItem *thisSHI =
reinterpret cast<TShellFileListItem* >(Items->Objects[ii]);
LPITEMIDLIST thisPIDL = thisSHI->pidl;
ShellExecute(Handle, "open", (char*)thisPIDL, 0, 0,
SW_SHOWDEFAULT);
Proszę znawców C++Buildera o zastanowienie się nad tym.
Aby przechowywane pod właściwością Objects[] pomocnicze obiekty TShellFileListItem zostały zwolnione w momencie destrukcji listy, konieczne jest przedefiniowanie jej metody DeleteString():
Wydruk 7.52. Zwalnianie pozycji listy
void __fastcall TSHFileListBox::DeleteString(int Index)
{
// pobierz wskaźnik do obiektu pomocniczego i zwolnij ten obiekt
TShellFileListItem *ShellItem = reinterpret_cast<TShellFileListItem*>
(Items->Objects[Index]);
delete ShellItem;
ShellItem = NULL;
// usuń tekst związany z pozycją
Items->Delete(Index);
}
Wspomnieliśmy przed chwilą, iż wynik zwracany przez metodę AddItem() równy jest szerokości dodawanej do listy pozycji (lub zeru, gdy pozycja nie zostanie dodana). Fakt ten wykorzystywany jest w metodzie ReadFileNames() odpowiedzialnej za skompletowanie listy - po wykonaniu poniższego fragmentu tej metody zmienna l zawiera szerokość najszerszej pozycji, co wykorzystywane jest później do rozstrzygnięcia, czy listę należy wyposażyć w poziomy pasek przewijania:
Wydruk 7.53. Obliczanie szerokości zajmowanej przez pozycje listy
ULONG celt = 1;
ULONG Fetched = 0;
ppenumIDList->Next(celt, &rgelt, &Fetched);
hExtent = 0;
while(Fetched > 0)
{
// dodaj pozycję do listy
int l = AddItem(rgelt);
if(l > hExtent)
{
hExtent = l;
}
ppenumIDList->Next(celt, &rgelt, &Fetched);
}
Obliczona szerokość zwiększana jest o dwupikselowy margines, a jeżeli właściwość ShowGlyphs ma wartość true - również o 18 pikseli na ikonę i jej odstęp od tekstu. Tak obliczona wartość przekazywana jest do listy pod postacią komunikatu LB_SETHORIZONTALEXTENT - jeżeli będzie ona większa od szerokości okna listy, u jej dolnej krawędzi automatycznie pojawi się pasek przewijania:
Wydruk 7.54. Wyposażenie listy w poziomy pasek przewijania
void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)
{
// dodaj minimalny margines
he += 2;
// jeżeli mają być widoczne ikony, dodaj 16 pikseli na ikonę
// i 2 piksele na jej odstęp od tekstu
if(ShowGlyphs)
he += 18;
// wyślij komunikat, który w razie potrzeby ustanowi
// poziomy pasek przewijania
Perform(LB_SETHORIZONTALEXTENT, he, 0);
}
Na tym kończy się repertuar nowych możliwości naszego komponentu. Poniżej przedstawiamy kompletny kod jego modułu źródłowego - znajduje się on również na dołączonej do książki płycie CD-ROM.
W związku z opisanymi usterkami poniższe listingi mogą ulec zmianom, należy je więc zweryfikować przed składem
Wydruk 7.55. SHFileListBox.h - plik nagłówkowy komponentu TSHFileListBox
//---------------------------------------------------------------------------
#ifndef SHFileListBoxH
#define SHFileListBoxH
//---------------------------------------------------------------------------
#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <FileCtrl.hpp>
#include <StdCtrls.hpp>
#include <ShlObj.h>
//---------------------------------------------------------------------------
class TShellFileListItem : public TObject
{
private:
LPITEMIDLIST Fpidl;
int FImageIndex;
public:
__fastcall TShellFileListItem(LPITEMIDLIST lpidl, int Index);
__fastcall ~TShellFileListItem(void);
__property LPITEMIDLIST pidl = {read=Fpidl};
__property int ImageIndex = {read=FImageIndex};
};
typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString
Item, bool &CanAdd);
class PACKAGE TSHFileListBox : public TFileListBox
{
private:
TImageList *FImages;
TNotifyEvent FOnDblClick;
bool FCanLaunch;
bool FRightBtnSel;
TAddItemEvent FOnAddItem;
int __fastcall AddItem(LPITEMIDLIST pidl);
void __fastcall GetSysImages(void);
protected:
DYNAMIC void __fastcall DblClick(void);
void __fastcall ReadFileNames(void);
DYNAMIC void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y);
void __fastcall DrawItem(int Index, const TRect &Rect, TOwnerDrawState State);
void __fastcall DoHorizontalScrollBar(int he);
DYNAMIC void __fastcall DeleteString(int Index);
public:
__fastcall TSHFileListBox(TComponent* Owner);
__fastcall ~TSHFileListBox(void);
__published:
__property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch, default=true};
__property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel,
default=true};
__property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick};
__property TAddItemEvent OnAddItem = {read=FOnAddItem, write=FOnAddItem};
};
extern "C" LPITEMIDLIST CopyPIDL(LPITEMIDLIST lpidl);
extern "C" unsigned short GetPIDLSize(LPITEMIDLIST lpidl);
extern "C" LPITEMIDLIST CreatePIDL(unsigned short size);
extern "C" LPITEMIDLIST GetNextItem(LPITEMIDLIST pidl);
//---------------------------------------------------------------------------
#endif
Wydruk 7.56. SHFileListBox.cpp - plik implementacyjny komponentu TSHFileListBox
// TSHFileListbox
// By: Sean Rock
// roks@bigfoot.com
// http://www.theblack.co.uk
// last modified: 11 Aug 00
//---------------------------------------------------------------------------
//#include <vcl.h>
#pragma hdrstop
#include "SHFileListBox.h"
#pragma package(smart_init)
//---------------------------------------------------------------------------
__fastcall TShellFileListItem::TShellFileListItem(LPITEMIDLIST lpidl, int Index)
: TObject()
{
// zachowaj kopię elementu systemowego „pidl” reprezentującego plik...
Fpidl = CopyPIDL(lpidl);
// .. oraz indeks ikony przeznaczonej dla pliku
FImageIndex = Index;
}
//----------------------------------------------------------------------------
__fastcall TShellFileListItem::~TShellFileListItem(void)
{
LPMALLOC lpMalloc=NULL;
if(SUCCEEDED(SHGetMalloc(&lpMalloc)))
{
// zwolnij pamięć zajętą przez element systemowy „pidl”
// reprezentujący plik
lpMalloc->Free(Fpidl);
lpMalloc->Release();
}
}
//---------------------------------------------------------------------------
__fastcall TSHFileListBox::TSHFileListBox(TComponent* Owner)
: TFileListBox(Owner)
{
ItemHeight = 18;
ShowGlyphs = true;
FCanLaunch = true;
FRightBtnSel = true;
}
//---------------------------------------------------------------------------
__fastcall TSHFileListBox::~TSHFileListBox(void)
{
// zwolnij listę ikon
if(FImages)
delete FImages;
FImages = NULL;
}
//----------------------------------------------------------------------------
void __fastcall TSHFileListBox::DeleteString(int Index)
{
// niniejsza metoda wywoływana jest w odpowiedzi na komunikat
// LB_DELETESTRING. Zwalnia ona łańcuch zawierający nazwę pliku oraz
// towarzyszący mu obiekt pomocniczy „pidl”
TShellFileListItem *ShellItem = reinterpret_cast<TShellFileListItem*>
(Items->Objects[Index]);
delete ShellItem;
ShellItem = NULL;
Items->Delete(Index);
}
//----------------------------------------------------------------------------
namespace Shfilelistbox
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TSHFileListBox)};
RegisterComponents("Samples", classes, 0);
}
}
//---------------------------------------------------------------------------
void __fastcall TSHFileListBox::ReadFileNames(void)
{
LPMALLOC g_pMalloc;
LPSHELLFOLDER pisf;
LPSHELLFOLDER sfChild;
LPITEMIDLIST pidlDirectory;
LPITEMIDLIST rgelt;
LPENUMIDLIST ppenumIDList;
int hExtent;
try
{
try
{
if(HandleAllocated())
{
GetSysImages();
// zablokuj uaktualnianie ekranu
Items->BeginUpdate();
// usuń całą zawartość listy
Items->Clear();
// pobierz wskaźnik systemowego alokatora pamięci
if(SHGetMalloc(&g_pMalloc) != NOERROR)
{
return;
}
// uzyskaj wskaźnik do interfejsu IShellFolder
if(SHGetDesktopFolder(&pisf) != NOERROR)
{
return;
}
// skonwertuj łańcuch zawierający nazwę foldera
// do postaci wymaganej przez OLE
WideChar oleStr[MAX_PATH];
FDirectory.WideChar(oleStr, MAX_PATH);
unsigned long pchEaten;
unsigned long pdwAttributes;
// uzyskaj obiekt „pidl” dla bieżącego foldera
pisf->ParseDisplayName(Handle, 0, oleStr, &pchEaten,
&pidlDirectory, &pdwAttributes);
// uzyskaj wskaźnik do interfejsu IShellFolder dla bieżącego foldera
if(pisf->BindToObject(pidlDirectory,NULL,
IID_IShellFolder, (void**)&sfChild) != NOERROR)
{
return;
}
// przeprowadź iterację po plikach w folderze
sfChild->EnumObjects(Handle, SHCONTF_NONFOLDERS |
SHCONTF_INCLUDEHIDDEN, &ppenumIDList);
// przetwórz listę pomocniczą uzyskaną w wyniku iteracji
ULONG celt = 1;
ULONG Fetched = 0;
ppenumIDList->Next(celt, &rgelt, &Fetched);
hExtent = 0;
while(Fetched > 0)
{
// dodaj element do listy
int l = AddItem(rgelt);
if(l > hExtent)
hExtent = l;
ppenumIDList->Next(celt, &rgelt, &Fetched);
}
}
}
catch(Exception &E)
{
throw(E); // ponów zaistniały wyjątek
}
}
__finally
{
// wykonaj czynności kończące dla mechanizmów systemowych
g_pMalloc->Free(rgelt);
g_pMalloc->Free(ppenumIDList);
g_pMalloc->Free(pidlDirectory);
pisf->Release();
sfChild->Release();
g_pMalloc->Release();
Items->EndUpdate();
}
// Utwórz poziomy pasek przewijania, jeżeli jest niezbędny
DoHorizontalScrollBar(hExtent);
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he)
{
// dodaj minimalny margines
he += 2;
// jeżeli mają być widoczne ikony, dodaj 16 pikseli na ikonę
// i 2 piksele na jej odstęp od tekstu
if(ShowGlyphs)
he += 18;
Perform(LB_SETHORIZONTALEXTENT, he, 0);
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::GetSysImages(void)
{
SHFILEINFO shfi;
DWORD iHnd;
if(!FImages)
{
FImages = new TImageList(this);
FImages->ShareImages = true;
FImages->Height = 16;
FImages->Width = 16;
iHnd = SHGetFileInfo("", 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX |
SHGFI_SHELLICONSIZE | SHGFI_SMALLICON);
if(iHnd != 0)
FImages->Handle = iHnd;
}
}
// ---------------------------------------------------------------------------
int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl)
{
SHFILEINFO shfi;
int Index;
SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL |
SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_DISPLAYNAME |
SHGFI_USEFILEATTRIBUTES);
// zweryfikuj nazwę pod kątem dodania jej do listy
bool FCanAdd = true;
if(FOnAddItem)
FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);
if(FCanAdd)
{
TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon);
Index = Items->AddObject(AnsiString(shfi.szDisplayName), (TObject*)ShellInfo);
// zwróć szerokość nazwy w pikselach
return Canvas->TextWidth(Items->Strings[Index]);
}
// zwróc zero jako sygnał, że nazwa nie została dodana
return 0;
}
// ---------------------------------------------------------------------------
void __fastcall TSHFileListBox::DrawItem(int Index, const TRect &Rect,
TOwnerDrawState State)
{
int Offset;
Canvas->FillRect(Rect);
Offset = 2;
if(ShowGlyphs)
{
TShellFileListItem *ShellItem = reinterpret_cast<TShellFileListItem*>
(Items->Objects[Index]);
// narysuj ikonę przeznaczoną dla pliku
FImages->Draw(Canvas, Rect.Left+2, Rect.Top+2, ShellItem->ImageIndex, true);
Offset += 18;
}
int Texty = Canvas->TextHeight(Items->Strings[Index]);
Texty = ((ItemHeight - Texty) / 2) + 1;
// wypisz nazwę pliku
Canvas->TextOut(Rect.Left + Offset, Rect.Top + Texty, Items->Strings[Index]);
}
//----------------------------------------------------------------------------
void __fastcall TSHFileListBox::DblClick(void)
{
if(FCanLaunch)
{
int ii=0;
// „uruchom” wszystkie podświetlone elementy
for(ii=0; ii < Items->Count; ii++)
{
if(Selected[ii])
{
AnsiString str = Items->Strings[ii];
ShellExecute(Handle, "open", str.c_str(), 0, 0, SW_SHOWDEFAULT);
}
}
}
// wygeneruj zdarzenie OnDblClick
if(FOnDblClick)
FOnDblClick(this);
}
//----------------------------------------------------------------------------
void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState Shift,
int X, int Y)
{
if(!FRightBtnSel)
return;
TPoint ItemPos = Point(X,Y);
// czy kursor myszy wskazuje jakąś nazwę?
int Index = ItemAtPos(ItemPos, true);
// nie, nic nie rób
if(Index == -1)
return;
// tak, wybierz wskazywaną pozycję
Perform(LB_SETCURSEL, (WPARAM)Index, 0);
}
//----------------------------------------------------------------------------
// ValidCtrCheck is used to assure that the components created do not have
// any pure virtual functions.
//
static inline void ValidCtrCheck(TSHFileListBox *)
{
new TSHFileListBox(NULL);
}
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// The following functions were written by Damon Chandler
//----------------------------------------------------------------------------
LPITEMIDLIST CopyPIDL(LPITEMIDLIST lpidl)
{
unsigned short size = GetPIDLSize(lpidl);
LPITEMIDLIST lpidlCopy = CreatePIDL(size);
MoveMemory(lpidlCopy, lpidl, size);
return lpidlCopy;
}
//----------------------------------------------------------------------------
unsigned short GetPIDLSize(LPITEMIDLIST lpidl)
{
unsigned short cb = 0;
while (lpidl)
{
cb = (unsigned short)(cb + lpidl->mkid.cb);
lpidl = GetNextItem(lpidl);
}
return (unsigned short)(cb + 2);
}
//----------------------------------------------------------------------------
LPITEMIDLIST CreatePIDL(unsigned short size)
{
LPITEMIDLIST lpidlResult;
LPMALLOC lpMalloc;
if (SUCCEEDED(SHGetMalloc(&lpMalloc)))
{
lpidlResult = (LPITEMIDLIST)lpMalloc->Alloc(size);
if (lpidlResult)
ZeroMemory(lpidlResult, sizeof(size));
lpMalloc->Release();
}
return lpidlResult;
}
//----------------------------------------------------------------------------
LPITEMIDLIST GetNextItem(LPITEMIDLIST pidl)
{
unsigned short nLen = pidl->mkid.cb;
if(nLen == 0)
return NULL;
return (LPITEMIDLIST)((LPBYTE)pidl + nLen);
}
Tworzenie własnych kontrolek bazodanowych
Tworzeniem kontrolek przeznaczonych do współpracy z bazami danych - zwanych popularnie kontrolkami bazodanowymi - rządzą te same zasady, które przestawiliśmy dotychczas w tym rozdziale. W zależności od charakteru tej współpracy kontrolki te podzielić można na dwie grupy: umożliwiające jedynie podgląd danych oraz umożliwiające również ich modyfikację.
Materiałem wyjściowym dla tworzonego przez nas komponentu TDBMaskEdit będzie kontrolka TMaskEdit, umożliwiająca wprowadzanie danych w postaci zgodnej z określoną a priori maską. Komponent ten wyposażymy najpierw w mechanizmy umożliwiające wyświetlanie danych zawartych w pliku źródłowym, następnie rozszerzymy jego funkcje o możliwości wprowadzania i modyfikowania danych.
Połączenie kontrolki z bazą danych
Dla kontrolki dokonującej wyświetlania i ew. edycji danych oczywistą sprawą jest konieczność zapewnienia mechanizmów łączności z tymi danymi. W bibliotece VCL szeroko pojętą organizacją dostępu do danych zajmuje się komponent TDataLink, postrzegający zawartość zbioru danych poprzez komponenty klasy TDataSet i TDataSource. „Skalarny” charakter naszej kontrolki TDBMaskEdit (wyświetlanie pojedynczej wartości) przesądza o zastosowaniu jej w zasadzie do pojedynczego pola rekordu bazy danych reprezentowanego przez komponent TField; łącznikiem pomiędzy kontrolką bazodanową a polem w bazie danych jest komponent TFieldDataLink, wywodzący się z klasy TDataLink.
Aby zrealizować połączenie kontrolki z danymi, konieczne jest wykonanie kolejno trzech następujących czynności:
zadeklarowanie właściwości odpowiedzialnych za połączenie - czyli wskazania na obiekt-łącznik oraz właściwości pokrewnych;
zaimplementowanie metod dostępowych tej właściwości;
zainicjowanie obiektu-łącznika.
Wykorzystanie klasy TFieldDataLink wymaga dołączenia pliku nagłówkowego dbctrls.hpp:
#include <DBCtrls.hpp> // dla TFieldDataLink
//---------------------------------------------------------------------------
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
TFieldDataLink *FDataLink;
...
};
Jako że nasz komponent bazuje na konkretnym polu bazy danych, konieczne jest zadeklarowanie dwóch związanych z tym właściwości: DataSource, określającej powiązany z bazą komponent TDataSource (reprezentujący bieżący rekord tej bazy) oraz DataField, określającej nazwę konkretnego pola w tym rekordzie:
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
....
AnsiString __fastcall GetDataField(void);
TDataSource* __fastcall GetDataSource(void);
void __fastcall SetDataField(AnsiString pDataField);
void __fastcall SetDataSource(TDataSource *pDataSource);
....
__published:
__property AnsiString DataField =
{read = GetDataField, write = SetDataField, nodefault};
__property TDataSource *DataSource =
{read = GetDataSource, write = SetDataSource, nodefault};
....
};
Wspominaliśmy na wstępie, iż nasz komponent powinien umożliwiać na żądanie blokadę modyfikacji danych, udostępniając je wyłącznie do podglądu; ten aspekt jego funkcjonowania nie jest jednakże elementem łączności z danymi - ustawienie na true właściwości ReadOnly uniemożliwia jedynie modyfikację wyświetlanej zawartości, co oczywiście nie zabrania użytkownikowi dokonywania bezpośrednich zapisów do pola bazy danych identyfikowanego przez właściwości DataSource i DataField.
Implementację metod dostępowych wspomnianych właściwości przedstawia wydruk 7.57.
Wydruk 7.57. Implementacja metod dostępowych właściwości połączeniowych
AnsiString __fastcall TDBMaskEdit::GetDataField(void)
{
return(FDataLink->FieldName);
}
TDataSource * __fastcall TDBMaskEdit::GetDataSource(void)
{
return(FDataLink->DataSource);
}
void __fastcall TDBMaskEdit::SetDataField(AnsiString pDataField)
{
FDataLink->FieldName = pDataField;
}
void __fastcall TDBMaskEdit::SetDataSource(TDataSource *pDataSource)
{
if(pDataSource != NULL)
pDataSource->FreeNotification(this);
FDataLink->DataSource = pDataSource;
}
Pewnego komentarza wymaga tu użycie funkcji FreeNotification(). Jak już wcześniej pisaliśmy, komponent musi być świadom zwalniania obiektów, na które wskazują jego właściwości. Świadomość tę zapewnia metoda Notification(), lecz tylko w stosunku do komponentów znajdujących się na tym samym formularzu; zwalnianie komponentów znajdujących się na innych formularzach nie jest dla niego zauważalne w sposób standardowy i musi być zapewnione przy użyciu metody FreeNotification(). Komponent, wywołując tę metodę w kontekście komponentu powiązanego, zapewnia sobie tym samym informację o destrukcji tego ostatniego. Wykorzystaliśmy tę możliwość, ponieważ nie można wykluczyć, iż powiązany komponent wskazywany przez właściwość DataSource znajduje się na innym formularzu.
Nasz komponent nie jest jeszcze kompletny - próba jego wykorzystania w aplikacji skończy się rychło komunikatem o naruszeniu mechanizmu ochrony (access violation). Oczywistym tego powodem jest brak egzemplarza obiektu-łącznika wskazywanego przez właściwość FieldDataLink. Egzemplarz ten powinien być utworzony w konstruktorze komponentu
__fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner)
: TMaskEdit(Owner)
{
FDataLink = new TFieldDataLink();
FDataLink->Control = this;
FDataLink->OnUpdateData = UpdateData;
FDataLink->OnDataChange = DataChange;
}
Po utworzeniu obiektu-łącznika zainicjowane zostają jego właściwości: Control, wskazująca macierzystą kontrolkę (czyli nasz komponent) i dwie właściwości zdarzeniowe, którym przypisuje się funkcje obsługi (będące metodami naszego komponentu).
Egzemplarz obiektu-łącznika powinien być także zwolniony w ramach destruktora, po uprzednim anulowaniu powiązań z naszym komponentem i jego metodami:
__fastcall TDBMaskEdit::~TDBMaskEdit(void)
{
if(FDataLink)
{
FDataLink->Control = 0;
FDataLink->OnUpdateData = 0;
FDataLink->OnDataChange = 0;
delete FDataLink;
}
}
Różnorodne atrybuty pola bazy danych, z którym połączony jest nasz komponent, dostępne są poprzez reprezentujący to pole komponent TField. Ten ostatni wskazywany jest przez właściwość Field, stanowiącą tak naprawdę powielenie właściwości obiektu łącznikowego o tej samej nazwie. Właściwość ta jest właściwością tylko do odczytu - jej deklaracja nie zawiera klauzuli write:
__property TField *Field = {read = GetField};
...
TField * __fastcall TDBMaskEdit::GetField(void)
{
return(FDataLink->Field);
}
Aktualizowanie zawartości kontrolki - zdarzenie OnDataChange
Nasz komponent, mimo połączenia z bazą danych, nie posiada jeszcze mechanizmów umożliwiających reagowanie na zmiany w powiązanych danych, na przykład „przesunięcie” bazy danych do kolejnego rekordu („wiersza”). Mechanizmy takie udostępniane są przez klasy łącznikowe - każda zmiana w połączonych danych powoduje mianowicie wygenerowanie zdarzenia OnDataChange w kontekście obiektu łącznikowego. Zdarzenie to jest typu TNotifyEvent, co decyduje o postaci jego funkcji obsługi, będącej de facto metodą DataChange() naszego komponentu:
class PACKAGE TDBMaskEdit : public TMaskEdit
{
private:
...
void __fastcall DataChange(TObject *Sender);
...
}
......
void __fastcall TDBMaskEdit::DataChange(TObject *Sender)
{
if(!FDataLink->Field) // czy okreslono obiekt łącznikowy?
{
// nie określono obiektu łącznikowego;
// w czasie projektowania zwróć nazwę komponentu,
// w czasie wykonania - pusty łańcuch
if(ComponentState.Contains(csDesigning))
Text = Name;
else
Text = "";
}
else // określono obiekt łacznikowy, zwróć tekstową reprezentację pola
Text = FDataLink->Field->AsString;
}
Metoda DataChange() sprawdza najpierw, czy w ogóle określono obiekt łącznikowy; jeżeli tak, to właściwości Text (dziedziczonej z klasy bazowej i reprezentującej wyświetlaną zawartość) nadawana jest tekstowa (AsString) reprezentacja odnośnego pola bazy. Jeżeli obiektu łącznikowego nie określono - FDataLink jest pustym wskaźnikiem - nie ma sensu pojęcie zawartości powiązanego pola i właściwości Text nadawane są wartości zastępcze: nazwa komponentu w czasie projektowania i pusty łańcuch w czasie wykonania.
Zapis zmian do bazy - zdarzenie OnUpdateData
Nasz komponent w obecnym kształcie zapewnia jedynie podgląd zawartości powiązanego pola, aktualizowany dzięki zdarzeniu OnDataChange(). Właściwość ReadOnly posiada domyślną wartość true, brak jest ponadto mechanizmów powodujących odzwierciedlenie zmian wyświetlanej zawartości w powiązanej bazie danych.
Oprócz oczywistego „odblokowania” komponentu (poprzez ustawienie na false właściwości ReadOnly) konieczna jest jeszcze modyfikacja odziedziczonej z klasy bazowej (TMaskEdit) reakcji na zdarzenia pochodzące od myszy i klawiatury. Zachowania te muszą obecnie odzwierciedlać powiązanie komponentu z bazą danych - w tym sensie, iż wszelkie zmiany w zawartości komponentu mają sens jedynie wtedy, jeżeli będą mogły być odzwierciedlone w zawartości powiązanego pola. Spełnienie tego warunku sprawdza się, próbując przełączyć powiązaną bazę danych w stan edycji - dokonuje tego metoda Edit() obiektu łącznikowego, zwracająca wartość true, jeżeli przełączenie takie faktycznie nastąpi.
Poniższy wydruk przedstawia zmodyfikowane metody MouseDown() i KeyDown() odpowiedzialne za reakcję komponentu na czynności wykonywane z udziałem myszy i klawiatury. W obydwu przypadkach po sprawdzeniu, iż dozwolone jest modyfikowanie zawartości komponentu (ReadOnly=false) i powiązana baza danych znajduje się w trybie edycji (FDataLink->Edit()=true), następuje powielenie zachowania odziedziczonego z klasy bazowej; w przeciwnym razie wywoływana jest funkcja obsługi zdarzenia (odpowiednio: OnMouseDown i OnKeyDown) określona przez użytkownika.
Wydruk 7.58. Obsługa myszy i klawiatury
void __fastcall TDBMaskEdit::MouseDown(TMouseButton Button, TShiftState Shift, int X, int Y)
{
if(!ReadOnly && FDataLink->Edit())
TMaskEdit::MouseDown(Button, Shift, X, Y);
else
{
if(OnMouseDown)
OnMouseDown(this, Button, Shift, X , Y);
}
}
void __fastcall TDBMaskEdit::KeyDown(unsigned short &Key, TShiftState Shift)
{
// klawisze sterujące kursorem
Set<unsigned short, VK_PRIOR, VK_DOWN> Keys;
Keys = Keys << VK_PRIOR << VK_NEXT << VK_END << VK_HOME << VK_LEFT << VK_UP
<< VK_RIGHT << VK_DOWN;
if(!ReadOnly && (Keys.Contains(Key)) && FDataLink->Edit())
TMaskEdit::KeyDown(Key, Shift);
else
{
if(OnKeyDown)
OnKeyDown(this, Key, Shift);
}
}
Niezależnie jednak od wszelkich działań wykonywanych na zawartości komponentu działania te pozostałyby bez wpływu na zawartość powiązanego pola, gdyby nie obsługa drugiego z opisywanych przed chwilą zdarzeń - OnUpdateData(). Mówiąc skrótowo, zdarzenie to generowane jest w rezultacie zmian poczynionych w ramach kontrolki (do sprawy tej za chwilę powrócimy) i stanowi okazję do odzwierciedlenia tych zmian w powiązanej bazie danych. Obsługą tego zdarzenia zajmuje się metoda UpdateData() prezentowana na wydruku 7.59. Po upewnieniu się, iż obiekt łącznikowy zezwala na modyfikację (FDataLink->CanModify), zawartość kryjąca się pod właściwością Text naszego komponentu przepisywana jest do pola bazy danych.
Wydruk 7.59. Zapisanie zmian w bazie danych
void __fastcall TDBMaskEdit::UpdateData(TObject *Sender)
{
if(FDataLink->CanModify)
FDataLink->Field->AsString = Text;
}
Powiązanie komponentu z bazą danych wymaga modyfikacji jeszcze jednego aspektu zachowania komponentu, dziedziczonego z klasy bazowej - mowa tu o metodzie Change(), której zadaniem jest poinformowanie Windows o zaistniałej zmianie zawartości. Otóż przełączenie bazy w tryb edycji (o ile nie znajduje się ona już w tym trybie) spowoduje wygenerowanie zdarzenia OnDataChange, co natychmiast doprowadzi do efektu odwrotnego od zamierzonego - mianowicie zastąpienie bieżącej wartości komponentu (dokładniej: jego właściwości Text) wartością odczytaną z bazy danych. Należy więc najpierw zabezpieczyć zarówno zawartość komponentu, jak i pozycję kursora w oknie edycyjnym i odtworzyć je po udanym przełączeniu w tryb edycji. Następnie należy ustawić w obiekcie łącznikowym wskaźnik informujący o zmodyfikowaniu zawartości bazy i wywołać odziedziczoną metodę Change().
Wydruk 7.60. Zmodyfikowana metoda Change()
void __fastcall TDBMaskEdit::Change(void)
{
if(FDataLink)
{
// należy zabezpieczyć właściwości Text i SelStart,
// gdyż przełączenie bazy w tryb edycji może je zmienić
AnsiString ChangedValue = Text;
int CursorPosition = SelStart;
if(FDataLink->CanModify && FDataLink->Edit())
{
// przywrócenie właściwości Text i SelStart
Text = ChangedValue;
SelStart = CursorPosition;
FDataLink->Modified(); // poinformowanie o zmianach
}
}
TMaskEdit::Change();
}
Uważny Czytelnik z pewnością zapyta w tym momencie - co tak naprawdę doprowadza do wygenerowania zdarzenia OnUpdateData() w obiekcie łącznikowym? Otóż w momencie zakończenia modyfikacji zawartości komponentu, czyli przeniesienia skupienia na inną kontrolkę, generowany jest komunikat CM_EXIT, który mapowany jest do metody CMExit():
BEGIN_MESSAGE_MAP
...
MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit)
...
END_MESSAGE_MAP(TMaskEdit)
Metoda ta, po zweryfikowaniu zawartości pola edycyjnego i upewnieniu się, że powiązana baza danych może być modyfikowana, wywołuje metodę UpdateRecord() obiektu łącznikowego:
void __fastcall TDBMaskEdit::CMExit(TWMNoParams Message)
{
try
{
ValidateEdit();
if(FDataLink && FDataLink->CanModify)
FDataLink->UpdateRecord();
}
catch(...)
{
SetFocus();
throw;
}
}
Treść metody TFieldDataLink::UpdateRecord() przekłada się głównie na wywołanie metody UpdateData()(nie mylić z metodą UpdateData() naszego komponentu):
procedure TDataLink.UpdateRecord;
begin
FUpdating := True;
try
UpdateData;
finally
FUpdating := False;
end;
end;
W klasie TFieldDataLink powoduje to wygenerowanie zdarzenia OnUpdateData:
procedure TFieldDataLink.UpdateData;
begin
if FModified then
begin
if (Field <> nil) and Assigned(FOnUpdateData)
then
FOnUpdateData(Self);
FModified := False;
end;
end;
Jakikolwiek wyjątek zaistniały w trakcie tej operacji powoduje przywrócenie skupienia naszemu komponentowi, czyli powrót do edycji.
Komunikat CM_GETDATALINK
Biblioteka VCL zawiera użyteczny komponent o nazwie TDBCtrlGrid. Umożliwia on wyświetlanie kolejnych rekordów bazy danych w swobodnym formacie - każdy rekord wyświetlany jest w odrębnej kontrolce, umiejscowionej na dedykowanym temu rekordowi panelu (rys. 7.5). Po zaktualizowaniu zbioru danych komponent TDBCtrlGrid wysyła do każdego panelu (i - rekurencyjnie - każdej znajdującej się na tym panelu kontrolki) komunikat CM_GETDATALINK, w odpowiedzi na który kontrolka powinna zwrócić wskaźnik do swego obiektu łącznikowego (lub zero, gdy takowego nie posiada). Wskaźnik ten wykorzystywany jest następnie do zaktualizowania właściwości DataSource obiektu łącznikowego:
Tu proszę wkleić rysunek AG-9-5.BMP
Rysunek 7.5. Panele kontrolki TDBCtrlGrid
Wydruk 7.61. Generowanie komunikatu CM_GETDATALINK
procedure TDBCtrlGrid.UpdateDataLinks(Control: TControl; Inserting: Boolean);
var
I: Integer;
DataLink: TDataLink;
begin
if Inserting and not (csReplicatable in Control.ControlStyle)
then
DatabaseError(SNotReplicatable);
DataLink := TDataLink(Control.Perform(CM_GETDATALINK, 0, 0));
if DataLink <> nil then
begin
DataLink.DataSourceFixed := False;
if Inserting then
begin
DataLink.DataSource := DataSource;
DataLink.DataSourceFixed := True;
end;
end;
if Control is TWinControl then
with TWinControl(Control) do
for I := 0 to ControlCount - 1 do
UpdateDataLinks(Controls[I], Inserting);
end;
Obsługa komunikatu CM_GETDATALINK wbudowana jest w każdą kontrolkę bazodanową, o czym można się przekonać, studiując chociażby treść modułu DBCTRLS.PAS. Obsługa taka wbudowana jest również i w nasz komponent TDBMaskEdit, by mógł on współpracować z przeglądarką TDBCtrlGrid. Odpowiedni fragment pliku nagłówkowego dokonuje mapowania komunikatu CM_GETDATALINK do metody CMGetDataLink:
BEGIN_MESSAGE_MAP
...
MESSAGE_HANDLER(CM_GETDATALINK, TMessage, CMGetDataLink)
...
END_MESSAGE_MAP(TMaskEdit)
która to metoda przypisuje wynikowemu polu struktury komunikatu adres obiektu łącznikowego:
void __fastcall TDBMaskEdit::CMGetDataLink(TMessage Message)
{
Message.Result = (int)FDataLink;
}
Na tym kończy się proces przekształcania „niezależnej” kontrolki edycyjnej TEditMask w kontrolkę bazodanową.
Rejestracja komponentów
Po skompletowaniu kodu źródłowego komponentu należy umieścić go w palecie komponentów, realizując wieloetapowy scenariusz zwany popularnie rejestracją.
Aby komponent mógł zostać zarejestrowany, nie może posiadać niezaimplementowanych, „czystych” (ang. pure) metod wirtualnych (lub dynamicznych) - metody takie deklaruje się często w klasach przeznaczonych nie do bezpośredniego wykorzystania, lecz na potrzeby tworzenia nowych komponentów, pozostawiając ich implementację klasom pochodnym. Deklaracja metody czysto wirtualnej ma następującą postać:
virtual <typ wyniku> __fastcall <nazwa_metody>(<lista parametrów>) = 0;
przy czym zamiast słowa kluczowego virtual wystąpić może słowo DYNAMIC. W celu stwierdzenia, czy klasa danego komponentu zawiera takie deklaracje, można przeprowadzić bezpośrednią analizę jej pliku nagłówkowego, można także powierzyć tę kontrolę jakiejkolwiek funkcji próbującej utworzyć egzemplarz odnośnego komponentu; w generowanym przez IDE kodzie funkcja taka nosi nazwę ValidCtrCheck(), a jej parametrem wywołania jest wskaźnik na egzemplarz komponentu.
Gdybyśmy przykładowo chcieli poddać opisanej kontroli komponent klasy TMyComponent, definicja wspomnianej funkcji miałaby postać:
static inline void ValidCtrCheck(TMyComponent *)
{
new TMyComponent(NULL);
}
Kompilator napotkawszy tę definicję, zasygnalizowałby dwa następujące błędy:
E2352 Cannot create instance of abstract class 'TCustomComponent'
E2353 Class 'TCustomComponent' is abstract because of 'function=0'
Drugi z komunikatów zawierałby nazwę niedozwolonej funkcji i mógłby wystąpić wielokrotnie; obydwa komunikaty odnosiłyby się do tej samej instrukcji, stanowiącej treść funkcji ValidCtrCheck.
Rejestracja komponentu dokonywana jest przez funkcję o nazwie Register, definiowaną w module źródłowym komponentu. Definicja tej funkcji musi być zamknięta w przestrzeni nazw (namespace) - nazwa tej przestrzeni musi być zgodna z nazwą modułu źródłowego i musi rozpoczynać się z wielkiej litery; reszta nazwy przestrzeni musi być pisana w całości małymi literami. Oto przykład funkcji rejestrującej komponent TDBMaskEdit zdefiniowany w module źródłowym DbMaskEdit.cpp:
namespace Dbmaskedit
{
void __fastcall PACKAGE Register()
{
TComponentClass classes[1] = {__classid(TDBMaskEdit)};
RegisterComponents("MJF Pack", classes, 0);
}
}
Nazwa funkcji (Register) musi być obowiązkowo poprzedzona wywołaniem makra PACKAGE.
Zasadniczą treścią funkcji Register() jest wywołanie funkcji RegisterComponents(), dokonującej fizycznej rejestracji wskazanych komponentów na wskazanej stronie palety komponentów. Deklaracja tej funkcji znajduje się w pliku nagłówkowym classes.hpp i ma następującą postać:
extern PACKAGE void __fastcall RegisterComponents
(const AnsiString Page,
TMetaClass* const * ComponentClasses,
const int ComponentClasses_Size
);
Pierwszy parametr określa nazwę strony (w palecie komponentów), na której umieszczone zostaną rejestrowane komponenty. Jeżeli strona o podanej nazwie jeszcze nie istnieje, zostanie automatycznie utworzona.
Drugi parametr jest tablicą, której elementy określają typy rejestrowanych komponentów. Każdy element jest wartością typu TMetaClass*, uzyskiwaną za pomocą operatora __classid, na przykład:
TMetaClass* MyRegComponents[3] = {
__classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
};
Zamiast TMetaClass* można użyć synonimu TComponentClass:
typedef TMetaClass* TComponentClass;
Ostatni parametr wywołania funkcji RegisterComponents() jest indeksem ostatniego elementu tablicy - jego wartość jest więc o jeden mniejsza od liczby rejestrowanych komponentów. Oto przykładowa rejestracja ww. trójki komponentów:
namespace Agcomponents
{
void __fastcall PACKAGE Register()
{
TMetaClass* MyRegComponents[3] = {
__classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
};
RegisterComponents("AGcomponents", MyRegComponents, 2);
}
}
W celu utworzenia tablicy rejestrowanych komponentów wygodnie będzie posłużyć się makrem OPENARRAY, zdefiniowanym w pliku nagłówkowym sysopen.h, symulującym tzw. tablicę otwartą (open array) Object Pascala - przedstawione wywołanie funkcji RegisterComponents() upraszcza się wówczas do postaci:
RegisterComponents("MyCustomComponents",
OPENARRAY( TMetaClass*,
( __classid(TAGcontrol1),
__classid(TAGcontrol2),
__classid(TAGcontrol3)
)
)
);
Jak widać, wywołanie makra OPENARRAY zastępuje drugi i trzeci parametr wywołania funkcji. Definicja makra OPENARRAY ogranicza do 19 wielkość generowanej tablicy, a więc i liczbę komponentów rejestrowanych w jednym wywołaniu funkcji RegisterComponents(). Nie jest to wielkim problemem, ponieważ funkcję RegisterComponents() wywoływać można wielokrotnie w treści funkcji Register().
Oprócz kompletnych komponentów procesowi rejestracji podlegać także mogą specyficzne typy właściwości oraz specjalizowane edytory komponentów. Zagadnieniem tym zajmiemy się szczegółowo w następnym rozdziale.
Jak wiadomo z rozdziału 8., klauzulę __declspec(delphiclass) zastąpić można wywołaniem makra DELPHICLASS, zdefiniowanego w pliku nagłówkowym sysmac.h - przyp. tłum.
Inspektor obiektów traktuje daną właściwość jako zdarzeniową (ang. event property), jeżeli stwierdzi, że typ powoływanego przez nią pola (lub metody) jest wskazaniem na metodę obiektu. Takim typem jest m.in. TNotifyEvent - przyp. tłum.
W rzeczywistości fragment ten pochodzi nie z deklaracji klasy TSpeedButton, lecz z przykładowego przycisku TExampleButton, którzy autorzy stworzyli (na bazie TGraphicControl) na użytek niniejszego rozdziału - przyp. tłum.
Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
1
2 D:\helion\C++Builder 5\R07-03.DOC