Chciałbym aby komponent miał kilka kolorów. Więc tak na prawdę kolory powinny byś właściwościami. Czy można właściwość zadeklarować jako tablicę?
Nie tyle można, co należy - aby uniknąć wielu pól klasy;
Zwróć uwagę na to w jaki sposób chcesz uwidocznić zestaw kolorów w okienku Inspektora Obiektów; Jeżeli grupa właściwości będzie osobnym obiektem to w okienku będzie widoczna jako cały węzeł, czyli tak jak w przypadku standardowej właściwości Font
w wielu podstawowych komponentach; Nie jest to złe, wręcz przeciwnie - w ten sposób można przedstawić zestaw wszystkich właściwości w postaci drzewka, grupując właściwości według danych kategorii;
A teraz co zrobiłeś źle; Po pierwsze, zadeklaruj klasę podobiektu jako dziedziczącą z klasy TPersistent
; Jej funkcjonalność jest potrzebna - o tym później; Czyli ramka klasy dla subgrupy właściwości:
type
TMyPanelColors = class(TPersistent)
public
constructor Create();
destructor Destroy(); override;
end;
W konstruktorze będzie można zainicjować wartości pól, załadować zasoby itd., wszystko co na początku potrzeba określić; W destruktorze zwolnić co trzeba, jeżeli cokolwiek trzeba; W Twoim przypadku klasa będzie przechowywać jedynie kolory, więc destruktora nie trzeba nadpisywać, ale zostawię go "na zaś";
Teraz zajmijmy się kolorami; Do ich przechowywania można bez problemu użyć macierzy; Aby całość była czytelna, macierz kolorów będzie indeksowana typem wyliczeniowym; Poniżej deklaracja:
type
TMyPanelColorKind = (mpckClient, mpckBorder, mpckShadow);
TMyPanelColorsArr = array [TMyPanelColorKind] of TColor;
Pierwszy typ to typ wyliczeniowy z indeksami kolorów, drugi to typ macierzy kolorów, indeksowany tym pierwszym; Typy zadeklarowane, więc wracamy do naszej klasy; Trzeba zadeklarować pole do przechowywania kolorów oraz właściwości dające do nich dostęp; Macierz nie może być właściwością, więc każdy kolor musi być właściwością osobną; Każda z nich będzie uzyskiwać dostęp do konkretnego pola macierzy za pomocą tych samych metod - dostępowej i zmieniającej:
type
TMyPanelColors = class(TPersistent)
private
FColors: TMyPanelColorsArr;
private
function GetPanelColor(AKind: TMyPanelColorKind): TColor;
procedure SetPanelColor(AKind: TMyPanelColorKind; AColor: TColor);
public
constructor Create();
destructor Destroy(); override;
published
property Client: TColor index mpckClient read GetPanelColor write SetPanelColor;
property Border: TColor index mpckBorder read GetPanelColor write SetPanelColor;
property Shadow: TColor index mpckShadow read GetPanelColor write SetPanelColor;
end;
Jak widzisz każda właściwość określa typ koloru za pomocą enuma, umieszczonego po słówku Index; Ten enum będzie przekazywany do metod GetPanelColor
oraz SetPanelColor
w parametrze AKind
; Dzięki temu jedna metoda może służyć do odczytu wartości trzech kolorów, a druga do modyfikacji dowolnego z trzech kolorów; Ich definicja jest dość trywialna - skoro mamy indeks koloru (enum w parametrze AKind
) to wiemy o którą komórkę macierzy chodzi; W drugiej metodzie, skoro mamy parametr AColor
to wiemy co wpisać do komórki macierzy przy modyfikacji wartości;
Ciała metod dostępowej i zmieniającej poniżej:
function TMyPanelColors.GetPanelColor(AKind: TMyPanelColorKind): TColor;
begin
Result := FColors[AKind];
end;
procedure TMyPanelColors.SetPanelColor(AKind: TMyPanelColorKind; AColor: TColor);
begin
if FColors[AKind] <> AColor then
begin
FColors[AKind] := AColor;
end;
end;
Pierwsza metoda zwraca w rezultacie wartość pobraną bezpośrednio z zadanej komórki macierzy; Druga natomiast najpierw sprawdza czy nowy kolor jest inny od bieżącego i jeśli jest - aktualizuje wartość komórki; Po co istnieje napiszę w dalszej części;
Jeszcze zostało zainicjowanie wartości początkowych pól macierzy - do tego mamy konstruktor:
constructor TMyPanelColors.Create();
begin
inherited Create();
FColors[mpckClient] := clWhite;
FColors[mpckBorder] := clBlue;
FColors[mpckShadow] := clLtgray;
end;
To w sumie tyle; Teraz pozostało stworzyć mechanizm, który umożliwi prawidłową obsługę takiej sub-właściwości; Póki co klasa potrafi przechowywać kolory, poprawnie je inicjować, odczytywać oraz modyfikować; Jednak klasa główna (komponentu) póki co nie będzie wiedziała o tych zmianach, więc trzeba napisać coś, co będzie ją o zmianach informować;
Tutaj można zadziałać na różne sposoby; Lazarus czasem głupieje, jeśli zrobi się coś nie tak jak to z góry ustalone, więc mechanizm notyfikacji napiszmy mniej więcej tak, jak ma to miejsce w LCL;
Co mamy i co potrzebujemy; Mamy klasę przechowującą kolory; Zmiana danego koloru musi oznaczać powiadomienie klasy komponentu; Komponent na taką zmianę musi zareagować; Zmiana koloru to niewielka zmiana i oznacza jedynie konieczność przemalowania komponentu, aby nowy kolor mógł zostać użyty; Klasa komponentu ma dostęp do klasy kolorów, więc nie trzeba jej dostarczać żadnych dodatkowych informacji; Dlatego też wystarczy samo poinformowanie o zmianie;
Zwykle robi się to za pomocą zdarzenia; Jest to najprostszy sposób, bo oznacza deklarację w klasie sub-właściwości pola zdarzenia, właściwości read-write, aby można było wpisać do niej własną metodę, a także prywatnej/chronionej metody wywołującej zdarzenie, jeśli takowe zostało do pola wpisane; My nie potrzebujemy przekazywać żadnych informacji w parametrach zdarzenia, więc typ zdarzenia będzie bardzo prosty:
type
TOnMyPanelColorChange = procedure() of object;
Do tego typu będą pasować wszystkie bezparametrowe metody proceduralne i takiej też użyjemy; Ale najpierw zajmijmy się klasą kolorów - dodajmy więc obsługę zdarzenia do tej klasy:
type
TMyPanelColors = class(TPersistent)
private
FColors: TMyPanelColorsArr;
FOnColorChange: TMyPanelOnColorChange;
private
function GetPanelColor(AKind: TMyPanelColorKind): TColor;
procedure SetPanelColor(AKind: TMyPanelColorKind; AColor: TColor);
private
procedure ChangeColor();
public
constructor Create();
destructor Destroy(); override;
published
property Client: TColor index mpckClient read GetPanelColor write SetPanelColor;
property Border: TColor index mpckBorder read GetPanelColor write SetPanelColor;
property Shadow: TColor index mpckShadow read GetPanelColor write SetPanelColor;
public
property OnColorChange: TMyPanelOnColorChange read FOnColorChange write FOnColorChange;
end;
Pole FOnColorChange
przechowuje wskaźnik na metodę; Właściwość OnColorChange
umożliwia dostęp do pola, natomiast metoda ChangeColor
służy do wywołania zdarzenia z pola; Metoda ChangeColor
nie jest wymagana, jednak będzie maskować sprawdzanie zawartości pola; Zdarzenie może być wywołane jedynie w przypadku, gdy pole FOnColorChange
faktycznie posiada przypisaną metodę; Jej kod jest bardzo prosty:
procedure TMyPanelColors.ChangeColor();
begin
if Assigned(FOnColorChange) then
FOnColorChange();
end;
Gdyby zdarzenie posiadało parametry to należałoby je przekazać w wywołaniu FOnColorChange()
; Pole zdarzenia można zainicjować wartością Nil w konstruktorze klasy, choć nie jest to wymagane; Jednak warto się zabezpieczyć, aby nie mieć w przyszłości problemów z szukaniem bugów:
constructor TMyPanelColors.Create();
begin
inherited Create();
FColors[mpckClient] := clWhite;
FColors[mpckBorder] := clBlue;
FColors[mpckShadow] := clLtgray;
FOnColorChange := nil;
end;
Na koniec, skoro już mamy oprogramowane wywoływanie zdarzenia, pasowałoby je faktycznie gdzieś wywoływać; Jedynym takim miejscem jest metoda modyfikująca wartość w macierzy kolorów; Tam też dodajmy wywołanie zdarzenia - nie bezpośrednio, bo po to mamy metodę ChangeColor
:
procedure TMyPanelColors.SetPanelColor(AKind: TMyPanelColorKind; AColor: TColor);
begin
if FColors[AKind] <> AColor then
begin
FColors[AKind] := AColor;
ChangeColor();
end;
end;
Teraz powinieneś widzieć sens istnienia warunku; Zdarzenie zmiany koloru zostanie wywołane tylko jeśli nowy kolor jest inny niż bieżący - po to właśnie istnieje ten warunek; W tym konkretnym przypadku nie jest to jakoś szczególnie potrzebne, dlatego że przemalowanie komponentu jest bradzo szybkie; Jednak w przypadku, gdy modyfikacja danej właściwości niesie za sobą wiele czasochłonnych zmian, takie zabezpieczenie jest konieczne, aby komponent wielokrotnie nie wykonywał danego kodu, jeśli nie jest to wymagane;
To by było na tyle, jeśli chodzi o klasę sub-właściwości; Teraz należy odpowiednio wykorzystać ją w głównej klasie komponentu; Najpierw ramka klasy panelu:
type
TMyPanel = class(TCustomControl)
private
FPanelColors: TMyPanelColors;
private
procedure SetPanelColors(APanelColors: TMyPanelColors);
procedure ColorChange();
protected
procedure Paint(); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy(); override;
published
property PanelColors: TMyPanelColors read FPanelColors write SetPanelColors;
end;
Konstruktor musi przyjmować w parametrze referencję do komponentu rodzica/właściciela; Mamy pole FPanelColors
, które zawiera instancję klasy sub-właściwości z kolorami; Mamy właściwość PanelColors
- ona widoczna będzie w oknie Inspektora Obiektów; Metoda ColorChange
jest naszym call-backiem, który będzie zdarzeniem klasy sub-właściwości, wywoływanym po zmianie koloru;
W konstruktorze należy utworzyć obiekt klasy sub-właściwości oraz wpisać wskaźnik na metodę ColorChange
do dedykowanej właściwości; W destruktorze należy zwolnić obiekt z pamięci:
constructor TMyPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Parent := AOwner as TWinControl; // to może nie być potrzebne
FPanelColors := TMyPanelColors.Create();
FPanelColors.OnColorChange := @Self.ColorChange;
end;
destructor TMyPanel.Destroy();
begin
FPanelColors.Free();
inherited Destroy();
end;
Teraz metoda SetPanelColors
- ona odpowiada za aktualizację obiektu sub-właściwości:
procedure TMyPanel.SetPanelColors(APanelColors: TMyPanelColors);
begin
FPanelColors.Assign(APanelColors);
end;
Jedynym jej zadaniem jest "skojarzenie" obiektów za pomocą Assign
; Metoda ta jest wymagana, ale nie istnieje w klasie TObject
- dlatego właśnie użyliśmy klasy TPersistent
i z niej została ona odziedziczona; Teraz metoda ColorChange
:
procedure TMyPanel.ColorChange();
begin
inherited Invalidate();
end;
Też nic nadzwyczajnego - wywołanie Invalidate
spowoduje przemalowanie komponentu, bo tyle należy wykonać po zmianie danego koloru; Metoda Invalidate
wykonuje kupkę przydatnych rzeczy, ale najważniejszą jest wowołanie metody Paint
, która to umożliwia malowanie komponentu; Dlatego też w tej metodzie należy oprogramować malowanie komponentu; Przykład poniżej:
procedure TMyPanel.Paint();
var
rctPanel: TRect;
begin
rctPanel := Rect(0, 0, Width - 10, Height - 10);
with Canvas do
begin
Brush.Color := FPanelColors.Shadow;
FillRect(rctPanel);
OffsetRect(rctPanel, 10, 10);
Pen.Color := FPanelColors.Border;
Brush.Color := FPanelColors.Client;
Rectangle(rctPanel);
end;
end;
Proste malowanie - najpierw cień, następnie ramka z wypełnieniem; Jak widzisz kolory pobierane są z obiektu FPanelColors
, do którego mamy dostęp z każdego miejsca głównej klasy komponentu;
Wszystko gotowe, teraz wystarczy zarejestrować komponent, instalując go na palecie komponentów; Nie będę wyjaśniał tego jak instaluje się komponenty, bo to raczej wiesz; A jeśli nie to wszystko opisane jest w dokumentacji - większość wystarczy wyklikać; Po zainstalowaniu komponentu i położeniu go na formę, w oknie Inspektora Obiektów zobaczysz swoją sub-właściwość jako węzeł, możliwy do rozwinięcia;
Jak dobrze pójdzie (czyli kod udało się napisać prawidłowo) to panel powinien wyglądać mniej więcej tak:
Cały kod dostępny jest na pastebin, nie jako gotowy moduł, a po prostu kod; Po utworzeniu modułu dla nowego komponentu, przeklej deklaracje typów i klas do sekcji Interface, a resztę do Implementation;
Wszystko co napisałem wyżej, napisane jest z pamięci, więc w razie jakichś błędów pisz, a poprawi się; Pewnych rzeczy w ogóle nie opisałem, aby zbytnio nie przedłużać (np. określenie domyślnych wartości właściwości, określanie ikonki dla komponentu na palecie w IDE itd.); To sobie możesz doczytać we własnym zakresie.