Pomalowanie każdego piksela na inny kolor

0

Trochę odbiegam od tego co robiłem do tej pory. Chciałem sobie pomalować jeden TPanel na dwa różne kolory, ale nie mając pomysłu jak to zrobić kombinując, spróbowałem nałożyć dwa kolejne komponenty TPanel na ten jeden i ustawić im różne kolory. To oczywiście zadziałało, ale nie daje efektu który chciałem osiągnąć, bo już teraz nic nie będzie widoczne co było umieszczone na tym panelu.

Czy jest możliwość np. stworzenia klasy dziedziczącej po TPanel i nadpisać metodę rysowania? Jeśli tak, to w jaki sposób odnieść się później do każdego piksela oddzielnie? Jeśli moje myśli idą w złym kierunku to naprowadźcie mnie.

0

A musisz to robić koniecznie na klasie TPanel? Nie możesz sobie po prostu napisać własnego komponentu, który będzie tłem? Potrzebujesz w ogóle funkcjonalności panelu, czyli np. grupowania komponentów?

Komponenty mają Canvas, a ta z kolei właściwość Pixels; Jednak jest to bardzo powolne rozwiązanie; Lepszym sposobem jest tzw. back-buffering, czyli malowanie wszystkiego na pomocniczej bitmapie, a na koniec namalowanie bitmapy na kanwie; Klasa TBitmap posiada właściwość ScanLine, która zwraca pointer na pierwszy piksel danego wiersza; Jest to najszybsze możliwe rozwiązanie;

PS: Jeśli już koniecznie musisz malować zwykły panel i nie chcesz napisać swojego komponentu to skorzystaj ze zdarzenia OnPaint klasy panelu; Tylko nie wołaj Inherited, bo to sensu nie ma; Przykład:

procedure TForm1.Panel1Paint(Sender: TObject);
begin
  with TPaintBox(Sender).Canvas do
  begin
    Pen.Color := clGray;
    Brush.Color := clWhite;

    Rectangle(ClipRect);
  end;
end;

Dzięki temu panel będzie miał szarą ramkę i białe wypełnienie:

panel.png

Natomiast dostęp do pojedynczych pikseli dostępny jest w takim przypadku jedynie za pomocą właściwości Canvas.Pixels; Tak więc wykorzystanie bitmapy pomocniczej do operowania na pojedynczych pikselach będzie dużo szybsze.

0

W Twoim kodzie nie działa mi wszystko tak jak na obrazku. Wygląd panelu całego się nie zmienia, jedynie w lewym górnym rogu powstaje mały prostokącik wyglądający jak na grafice.

Co do Bitmapy, to niestety nie wiem jak się nią zająć. Kompletnie nie wiem w którym kierunku iść. Na razie doszedłem do pomalowania płótna panelu na dwa kolory.

0

Nie odpowiedziałeś mi na pytanie, czy grupowanie komponentów jest Ci w ogóle potrzebne.

0

Pisałem wcześniej komentarz, ale zobaczyłem że swój post edytowałeś i go usunąłem.

Tych komponentów o takim samym malowaniu ma być kilkanaście, a na nich, na każdym z nich co najmniej 1, więc wydaje mi się że jednak to musi być TPanel.

1

@dani17 - nie ma znaczenia, czy na tych malowanych komponentach będą się znajdować inne, bo tak to może wyglądać zarówno ze zwykłymi panelami, jak i pseudo-panelami graficznymi (np. taki głupi TImage jako wizualny panel czy po prostu tło);

Pytam, dlatego że klasa TPanel nie służy jedynie do wizualnego grupowania komponentów; Umożliwia ich osadzanie w liście własnych kontrolek, dostępnych poprzez właściwość Controls; Dzięki temu ukrycie panelu powoduje automatyczne ukrycie wszystkich kontrolek znajdujących się na nim; Inna rzecz - gdy przesuniemy panel, przesunie się razem z całą swoją zawartością; Kontrolki graficzne tego nie umożliwiają;

Proponuję napisanie własnego komponentu, bez względu na to czy potrzebujesz osadzać kontrolki w tych panelach czy nie; Klasa, z której będzie dziedziczyć Twój komponent to TCustomControl; Malowanie komponentu wykonuj w metodzie Paint, którą musisz nadpisać za pomocą Override;

W razie problemów pytaj.

0

Będę więc kombinował, ale to jutro. Jeszcze wcześniej nie tworzyłem własnych komponentów, więc trochę dłużej to zajmie.
Akurat przesuwanie całego Panelu wraz z komponentami położonymi na nim jest dla mnie ważne.

0

Pierwsze pytanie co do tworzenie komponentów. 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ę?

A dalej utknąłem. Chociaż pewnie jakieś głupoty narobiłem

 
unit MyPanel;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs;

type
  TColorsList = class
    private
      FColor1: TColor;
      FColor2: TColor;
      FColor3: TColor;
    published
      property Color1: TColor read FColor1 write FColor1 default clBlack;
      property Color2: TColor read FColor2 write FColor2 default clWhite;
      property Color3: TColor read FColor3 write FColor3 default clBlack;
  end;

  TMyPanel = class(TCustomControl)
  private
    { Private declarations }
    FLeft: Integer;
    FRight: Integer;
    FWidth: Integer;
    FHeight: Integer;
    FCaption: String;
    FColors: TColorsList;
    FFont: TFont;
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Paint; override;
  published
    { Published declarations }
    property Left: Integer read FLeft write FLeft;
    property Right: Integer read FRight write FRight;
    property Width: Integer read FWidth write FWidth;
    property Height: Integer read FHeight write FHeight;
    property Caption: String read FCaption write FCaption;
    property Colors: TColorsList read FColors write FColors;
    property Font: TFont read FFont write FFont;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Standard',[TMyPanel]);
end;

constructor TMyPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FColors := TColorsList.Create;
  ControlStyle := ControlStyle + [csOpaque];
end;

destructor TMyPanel.Destroy;
begin
  inherited;
  FColors.Free;
end;

procedure TMyPanel.Paint;
begin
  Canvas.Brush.Color := Colors.Color1;
  Canvas.Rectangle(Rect(0, 0, Width, Height div 2));
  Canvas.Brush.Color := Colors.Color2;
  Canvas.Rectangle(Rect(0, Height div 2, Width, Height));
  Canvas.TextOut(0, 0, Caption);
end;

end.     

Po zainstalowaniu i próbie umieszczenia na formie nic się nie pojawia po uruchomieniu programu. W inspektorze obiektów nie mam właściwości Colors, a nie mogę rozwinąć właściwości Font. Przy próbuje przypisania czegokolwiek do Font w inspektorze dostaję komunikat "Cannot assign a Nil to TFont". Nie mogę też umieścić innych komponentów na moim panelu.

Edit
Aby umieścić na komponencie inne kontrolki musiałem dodać [csAcceptsControls] do ControlStyle i działa. Komponent pojawia się również już w programie. Usunąłem 4 pierwsze właściwości. One i tak się wyświetlają w inspektorze, a po umieszczeniu ich w kodzie, później edycja w inspektorze nic nie dawała. Pozostaje problem z kolorami i czcionką.

0

A czytałeś to:
Rozdział 15
?

0

Czytałem. I starałem się zastosować do tego ale może coś przeoczyłem.

2

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:

mypanel.png

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.

0

Raczej dobrze zrozumiałem tylko pierwszą część, czyli w zasadzie to co jest mi potrzebne, choć i tak mam dwa pytania. Druga część jest dla mnie dość skomplikowana ale staram się to rozgryźć.

type 
  TMyPanelColors = class(TPersistent)
  private
    FColors: TMyPanelColorsArr;
    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;
  end;

Tak na prawdę można by było właściwości Client przy odczytywaniu i nadpisywaniu umieścić po prostu FColors, prawda? Chociaż dopiero teraz widzę, że to inny typ. Więc może dałoby się to zrobić FColors[TMyPanelColorKind].

Drugie dotyczy tego fragmentu:

procedure TMyPanel.SetPanelColors(APanelColors: TMyPanelColors);
begin
  FPanelColors.Assign(APanelColors);
end;
 

To jest powiązanie klasy MyPanel z klasą MyPanelColors? Ale dlaczego odbywa się akurat w metodzie SetPanelColors?

Analizuję teraz ten kod od tyłu i pojawiło się jeszcze jedno pytanie. Początkowo myślałem, że chodzi o to żeby kolory zmieniały się właściwie już jak użytkownik docelowego programu będzie je zmieniał, ale teraz dostrzegam czy czasem nie chodzi o to żeby po zmianie w inspektorze obiektów kolory nie zmieniały sie automatycznie na formie? Ogólnie nie jestem w stanie pojąć o co chodzi z tymi zmianami koloru. Jest np metoda TMyPanel.ColorChange();, której wywołania nie widzę nigdzie. Ostatnia chyba sprawa jest taka, że nie wiem co tak na prawdę ma się stać poprzez FOnColorChange();.

Dodatkowo mam jeszcze problem z ustawieniem wartości domyślnych. Po dopisaniu na końcu deklaracji właściwości default 2; i uożeniu komponentu na formie, w inspektorze obiektów ta wartość wynosi 0.

2

Tak na prawdę można by było właściwości Client przy odczytywaniu i nadpisywaniu umieścić po prostu FColors, prawda?

Nie wiem jak to rozumieć; Nie można stworzyć upublicznionej właściwości, która będzie macierzą, czyli czegoś takiego:

published
  property AllColors: TPanelColorsArr read FColors write FColors;

Bo dostaniesz błąd kompilacji o poniższej treści:

Error: This kind of property cannot be published

Dlatego też trzeba ją rozbić - zadeklarować każdy kolor z osobna; Natomiast do aktualizacji pola macierzy potrzebna jest metoda, bo trzeba nie tylko zaktualizować wartość, ale też wywołać metodę "powiadamiającą" główną klasę o modyfikacji (klasa główna sama o tym nie będzie wiedziała);

Natomiast metoda GetPanelColor nie jest wymagana, bo możesz w tym miejscu po prostu wstawić nazwę pola (macierzy) i w nawiasach kwadratowych podać konkretny indeks:

published
  property Client: TColor read FColors[mpckClient] write SetPanelColor;

Jednak z metodą wygląda lepiej, czytelniej;

To jest powiązanie klasy MyPanel z klasą MyPanelColors?

Nie, to jest modyfikacja pola (obiektu) FPanelColors, w celu aktualizacji danych w nim zawartych; Pole wywołuje metodę Assign, aby pobrać dane z obiektu tego samego typu z parametru APanelColors; Nie tworzy nowego obiektu, a po prostu kopiuje dane;

Ale dlaczego odbywa się akurat w metodzie SetPanelColors?

A dasz radę wywołać metodę Assign bezpośrednio w deklaracji właściwości? No właśnie, dlatego potrzebna jest metoda; Zresztą w tym miejscu można wykonywać jeszcze inne czynności, więc taka metoda przydaje się podwójnie;

Jeśli nie zapiszesz w ogóle mutatora, czyli zadeklarujesz właściwość tak:

published
  property PanelColors: TPanelColors read FPanelColors;

to cała ta gałąź będzie tylko do odczytu - w oknie Inspektora Obiektów zobaczysz jej nazwę wyszarzoną; Podczas projektowania zmiany kolorów będą widoczne w designerze, ale po uruchomieniu nie zostaną wczytane kolory, które wyklikałeś, czego efektem będą kolory domyślne (zdefiniowane w konstruktorze klasy sub-właściwości); Dlatego też metoda SetPanelColors jest potrzebna;

Początkowo myślałem, że chodzi o to żeby kolory zmieniały się właściwie już jak użytkownik docelowego programu będzie je zmieniał [...]

No i faktycznie tak będzie - po to m.in. pisany jest ten kod; Dodatkowo ten kod nieco komplikuje fakt, iż nowe właściwości znajdują się w pod-obiekcie; Owa komplikacja to jedynie utworzenie dodatkowego zdarzenia do powiadamiania oraz dwie dodatkowe metody, jedna wywołująca zdarzenie oraz druga, która tym zdarzeniem jest (ta z głównej klasy);

[...] ale teraz dostrzegam czy czasem nie chodzi o to żeby po zmianie w inspektorze obiektów kolory nie zmieniały sie automatycznie na formie?

Ten kod daje możliwość dodania nowej gałęzi właściwości do okienka Inspektora Obiektów; Jednocześnie daje możliwość ustawiania tych kolorów z poziomu kodu; To czy te właściwości będą widoczne w okienku czy nie, określa ich umiejscowienie; Aby były widoczne, muszą się znajdować w sekcji Published; Jeżeli przeniesiesz je do sekcji Public to w okienku Inspektora nie będą widoczne, ale nadal będzie do nich dostęp z poziomu kodu;

Natomiast za aktualizację interfejsu (podczas projektowania, jak i podczas działania programu) odpowiada nasz kod; Zmiana danego koloru to po kolei:

  • wywołanie metody SetPanelColor, która zmienia wartość w macierzy oraz:
  • wywołuje metodę ChangeColor, która z kolei:
  • wywołuje zdarzenie FOnColorChange, jeśli jest przypisane, a to z kolei:
  • wykonuje kod metody ColorChange z głównej klasy komponentu, która to:
  • wywołuje metodę Invalidate, która oprócz wielu operacji:
  • wywołuje metodę Paint, w której mamy zdefiniowane kroki malowania powierzchni komponentu;
    Tak więc jeśli użytkownik zmieni kolor, bez względu na to czy w okienku czy z poziomu kodu, kolor faktycznie zostanie zmieniony, klasa komponentu zostanie o tym fakcie poinformowana, dzięki czemu zostanie wywołana metoda malująca; Natomiast projektowanie formularza to nie tylko wizualizacja efektu końcowego - w tle wykonuje się kod formularzy, komponentów oraz z różnych innych miejsc; Dlatego też zmiana koloru komponentu podczas projektowania oznacza wykonanie kodu malującego kontrolkę, co od razu widzimy na projektowanym formularzu;

W razie czego zawsze możesz debugować kod komponentu, aby prześledzić jak działa; Jeśli tworzy się własne komponenty to jest to konieczność - w przypadku nieprawidłowego działania lub w innych przypadkach, jest to najlepsze rozwiązanie; Bo tworzenie pliku logów jest do d**y;


Napisałem nowy komponent, tym razem sprawdzając w środowisku czy działa prawidłowo; Kod całego modułu znajduje się tutaj; Po dodaniu paczki do IDE i przeinstalowaniu środowiska, nowy komponent będzie się znajdował w zakładce Test Components; Po położeniu na formę będzie wyglądać np. tak:

form-design.png

Natomiast w okienku Inspektora Obiektów zobaczysz nową gałązkę z właściwościami:

oi-window.png

Dodatkowo mam jeszcze problem z ustawieniem wartości domyślnych. Po dopisaniu na końcu deklaracji właściwości default 2; i uożeniu komponentu na formie, w inspektorze obiektów ta wartość wynosi 0.

Nie do tego to służy;

Wartości domyślne określa się w konstruktorze klasy komponentu - zobacz jak napisałem kod wcześniej; Natomiast to co znajduje się po słówku Default służyć ma środowisku (ogólnie ujmując); Jeżeli właściwość ma określoną wartość domyślną po słówku Default, można ją przywrócić w oknie Inspektora Obiektów, klikając prawym przyciskiem myszy na nazwę właściwości i wybierając odpowiednią pozycję z menu kontekstowego:

default.png

Jeżeli deklaracja właściwości nie będzie zawierać zapisu wartości domyślnej lub będzie zakończona słówkiem Nodefault to tej pozycji w menu kontekstowym po prostu nie będzie:

nodefault.png

W takim przypadku będziesz musiał grzebać w kodzie i poszukać sobie wartości domyślnej, jeśli zmieniłeś ją w okienku i nie pamiętasz jak była wartość początkowa; Domyślne wartości nie służą tylko i wyłącznie środowisku, ale o tym doczytaj z dokumentacji.

0

Wielkie dzięki, wyjaśniłeś tymi dwoma postami kilka rzeczy które dopiero przyszły mi do głowy.

Obecnie została mi chyba już tylko jedna niewiadoma.

wywołuje zdarzenie FOnColorChange, jeśli jest zainicjowane, a to z kolei:
wywołuje metodę ColorChange z głównej klasy komponentu...

nie wiem w którym miejscu FOnColorChande wywołuje ColorChange.

1

Najnowszy kod testowego komponentu znajduje się tutaj, więc na jego podstawie odpowiem na zadane pytanie;

Zmiana koloru zostaje dokonana w tej metodzie:

procedure TPanelColors.SetColor(AKind: TPanelColorKind; AColor: TColor);
begin
  if FColors[AKind] <> AColor then
  begin
    FColors[AKind] := AColor;
    DoColorChange();
  end;
end;

Oprócz modyfikacji wartości pola macierzy, wywoływana jest metoda DoColorChange; Ona wygląda tak:

procedure TPanelColors.DoColorChange();
begin
  if Assigned(FOnColorChange) then
    FOnColorChange();
end;

Czyli sprawdza czy zdarzenie zostało przypisane do pola i jeśli tak - zostaje wywołane; Natomiast metoda, której wskaźnik znajduje się w tym polu została wpisana poprzez właściwość OnColorChange w konstruktorze głównej klasy komponentu:

constructor TColorPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Parent := AOwner as TWinControl;

  // csOpaque         - zaznacza, że sami malujemy całą powierzchnię komponentu
  // csAcceptControls - zaznacza, że nasz komponent może przechowywać w sobie inne
  //                    kontrolki, czyli definiuje zachowanie takie jak w TPanel
  ControlStyle := ControlStyle + [csOpaque, csAcceptsControls];

  FPanelColors := TPanelColors.Create();

  // tutaj zostaje wpisany wskaźnik na metodę TColorPanel.ColorChange
  // ta metoda będzie wywoływana w metodzie TPanelColors.DoColorChange
  FPanelColors.OnColorChange := @ColorChange;
end;

W metodzie TColorPanel.ColorChange znajduje się wywołanie Invalidate, które woła Paint, czyli własny kod malujący; Wiem że to na początek może być zagmatwane, ale chciałeś mieć osobną gałąź własnych właściwości, więc... masz za swoje :]

Tak więc FOnColorChange zawiera wskaźnik na metodę ColorChange z głównej klasy komponentu, czyli klasa sub-właściwości pośrednio (za pomocą wskaźnika z pola FOnColorChange) wywołuje metodę TColorPanel.ColorChange.

0

Wszystko osobna ogarnąłem, ale sam jeszcze nie mogę tego przenieść na nic swojego. Będę kombinował dalej i jutro zobaczymy czy wyjdzie. Chociaż jeszcze zanim się tego nauczę to będę się musiał posiłkować tym tematem.

0

W razie czego na początek zrezygnuj z grupowania właściwości w sub-obiektach i deklaruj je bezpośrednio w klasie komponentu; Będzie łatwiej, bo odpadnie notyfikacja głównej klasy o zaistniałych zmianach;

Zacznij od czegoś prostego i powoli podnoś poprzeczkę.

0

W końcu do tego wróciłem, próbuje coś osiągnąć ale w tym momencie pojawił się błąd przy instalacji. cmdlinedebugger.pp(300,12) Error: Identifier not found "dsError" Nie mam pojęcia jak to obejść.

0

No dobrze, ale jakiego kodu używasz i kiedy ten błąd wyskakuje? I którą wersję Lazarusa i FPC posiadasz?

0

Błąd wyskakuje przy instalacji jakiegokolwiek komponentu. Nawet tego, którego kod Ty podałeś. Lazarusa mam najnowszego. Na starszym nie miałem problemów z instalacją.

0

Do tego mam kolejne pytanie.

Robię komponent TCustomControl. Rysuję na nim kilka napisów. Czy jest możliwość żeby po najechaniu na niego, zmieniał kolor i robiło się podkreślenie?

Tak samo mam na nim kilka prostokątów w różnych kolorach, czy po najechaniu na jeden z nich można zmienić kolor tylko tego jednego?

1

@dani17 - ale dlaczego pytasz "czy da się", skoro to oczywiste? :]

W momencie najechania kursorem nad powierzchnię komponentu, owa kontrolka otrzymuje komunikat CM_MOUSEENTER, a po zabraniu kursora znad kontrolki, komunikat CM_MOUSELEAVE; Dlatego też musisz te komunikaty odebrać i obsłużyć;

Typy komunikatów i ich stałe znajdują się w modułach LMessages oraz Controls; Ramka metod do obsługi wymienionych komunikatów wygląda tak:

protected
  procedure CMMouseEnter(var AMessage: TLMessage); message CM_MOUSEENTER;
  procedure CMMouseLeave(var AMessage: TLMessage); message CM_MOUSELEAVE;

procedure TMyControl.CMMouseEnter(var AMessage: TLMessage);
begin
  inherited CMMouseEnter(AMessage);
  { Twój kod obsługi komunikatu }
end;

procedure TMyControl.CMMouseLeave(var AMessage: TLMessage);
begin
  inherited CMMouseLeave(AMessage);
  { Twój kod obsługi komunikatu }
end;

Zresztą w ten sposób obsługuje się dowolne komunikaty; Wywołanie z Inherited jest potrzebne wtedy, gdy komunikat ma zostać przetrawiony w klasie, z której Twoja dziedziczy, czyli w większości przypadków; Nie dodawaj wywołania metody z klasy bazowej tylko wtedy, gdy jest to konieczne; No i oczywiste jest, że jeśli bazowa klasa (z której Twoja dziedziczy) nie posiada nadpisywanej metody, to siłą rzeczy nie da się jej wywołać;

Natomiast jeżeli chcesz dokonać jakiejś akcji podczas ustawienia kursora nad konkretnym fragmentem komponentu, należy obsłużyć komunikat LM_MOUSEMOVE, w taki sam sposób jak wyżej:

private
  procedure WMMouseMove(var AMessage: TLMMouseMove); message LM_MOUSEMOVE;

procedure TMyControl.WMMouseMove(var AMessage: TLMMouseMove);
begin
  inherited WMMouseMove(AMessage);
  { Twój kod obsługi komunikatu }
end;

Parametr AMessage posiada pole Pos typu TSmallPoint, a ono zawiera relatywne współrzędne kursora (punkt 0,0 określa lewy górny róg komponentu);

Przy okazji - jak nie wiesz w jakiej sekcji znajduje się dana metoda (np. WMMouseMove) to kliknij na jej nazwę lewym przyciskiem myszy, trzymając klawisz Ctrl - zostaniesz przeniesiony do metody bazowej; Możesz też to wykonać najeżdżając kursorem tekstowym i wciskając kombinację Alt+Up lub Alt+Down;

Jeśli Twoja obsługa tych komunikatów to po prostu przemalowanie komponentu, możesz sobie zadeklarować flagę np. FCursorAbove, w metodzie CMMouseEnter ustawiać ją na True a w CMMouseLeave na False; Dodatkowo pod wywołaniem bazowej, wrzucić Invalidate; Dzięki temu po najechaniu lub zabraniu kursora wywołasz pośrednio metodę Paint, w której użyj zadeklarowanej flagi do sposobu malowania kontrolki;

Nie musisz za każdym razem przemalowywać całego komponentu; Jeżeli zależy Ci w danym momencie - np. w metodzie LMMouseMove - na przemalowaniu fragmentu to możesz to zrobić bezpośrednio w tej metodzie, korzystając z właściwości Canvas, która w każdym miejscu kodu klasy jest dostępna.

0

No dobrze, zaraz się tym pobawię, ale napisałeś jak to będzie w zależności od współrzędnych. Na prostokątach myślę, że będzie to dosyć łatwo zrobić. Ale jak na napisach? Pytanie tylko teoretyczne, może ten sam sposób jednak zadziała, ale to muszę potestować najpierw.

PS. "polajkuje" wszystkie odpowiedzi przydatne jakoś później

1

Na prostokątach myślę, że będzie to dosyć łatwo zrobić. Ale jak na napisach?

To zależy od wymagań i wariacji;

Jeżeli "napis" będzie jednoliniowy to jego powierzchnia jest prostokątna; W przypadku wieloliniowego tekstu, możesz rozróżnić dwa przypadki - albo traktować cały tekst jako prostokąt, również puste miejsce na końcach linii, albo dla każdej linii wyznaczyć prostokątną powierzchnię;

Podobną funkcjonalność ma mój TFormatLabel, czyli opcję podświetlania i podkreślania linków; Każdy link dzieli się na linie, a dla każdej linii obliczana jest jej powierzchnia; Dzięki temu link podświetlany jest tylko i wyłącznie wtedy, gdy kursor znajduje się nad tekstem; To jest konieczność, dlatego że jeden link może być podzielony na dwa niezależne bloki, nie stykające się ze sobą (początkowy fragment na końcu linii, a końcowy fragment na początku kolejnej linii);

Do pomiaru szerokości i wysokości tekstu masz do dyspozycji metody Canvas.TextWidth i Canvas.TextHeight, które dokonują obliczeń na podstawie bieżących ustawień fontu, czyli danych zawartych we właściwości Font.

0

MouseMove działa prawie idealnie. Niestety strasznie mi napis mruga :/

1

Mruga, bo mu tak kazałeś; Założę się, że w każdym WMMouseMove przemalowujesz cały komponent lub jego fragment, bez względu na to czy potrzeba, czy nie potrzeba; Malowanie po ekranie to bardzo czasochłonna czynność i o miganie ("flickering") nie trudno :]


Załóżmy, że Twój komponent ma malowany ręcznie kwadrat na środku; Ten kwadrat ma zmienić kolor po najechaniu kursorem oraz przywrócić oryginalny kolor po zabraniu kursora znad jego powierzchni; Dane dotyczące współrzędnych i rozmiaru figury trzymamy w polu klasy:

private
  FSquareRect: TRect;

Ten rekord wypełniamy wstępnie w konstruktorze klasy kontrolki; Potrzebujemy też informacji, czy kursor w danym momencie znajduje się nad kwadratem; Deklarujemy więc flagę jako drugie pole klasy:

private
  FCursorInSquare: Boolean;

Wartość tego pola ustalamy jako False w konstruktorze; Teraz potrzebujemy zdefiniować metodę malującą sam kwadracik - przyda się osobna metoda, dlatego że będzie wywoływana w dwóch miejscach:

private
  procedure DrawSquare();

procedure TMyControl.DrawSquare();
begin
  with Canvas do
  begin
    Brush.Color := IfThen(FCursorInSquare, clBlue, clSilver);
    FillRect(FSquareRect);
  end;
end;

Metoda ta maluje kwadracik w miejscu zdefiniowanym przez rekord z pola FSquareRect; Kolor dobiera na podstawie stanu flagi FCursorInSquare - jeśli pole to zawiera wartość True, wybiera kolor niebieski, w przeciwnym razie szary;

Następna w kolejne jest metoda Paint - ona wywoływana jest wtedy, gdy zaistnieje potrzeba przemalowania całej powierzchni komponentu; Dlatego też musi malować nie tylko kwadracik, ale też całe tło i ogólnie wszystko, co ma ta kontrolka wyświetlać:

protected
  procedure Paint(); override;
  
procedure TMyControl.Paint();
begin
  with Canvas do
  begin
    { tło komponentu }
    Brush.Color := clWhite;
    FillRect(ClientRect);

    { kwadrat }
    DrawSquare();
  end;
end;

Jej kod jest prosty - najpierw wypełnia całe tło na biało, a następnie maluje kwadracik za pomocą przygotowanej metody DrawSquare;

Ostatnie co potrzeba dodać to odpowiednie ustawianie wartości pola FCursorInSquare i przemalowywanie samej powierzchni kwadratu; To będzie wykonywane podczas wykrycia ruchu kursora nad kontrolką, czyli w metodzie WMMouseMove:

private
  procedure WMMouseMove(var AMessage: TLMMouseMove); message LM_MOUSEMOVE;
  
procedure TMyControl.WMMouseMove(var AMessage: TLMMouseMove);
var
  LInSquare: Boolean;
begin
  inherited WMMouseMove(AMessage);

  { kursor w kwadracie }
  LInSquare := PtInRect(FSquareRect, SmallPointToPoint(AMessage.Pos));
  
  { stan zmienił się }
  if FCursorInSquare <> LInSquare then
  begin
    { zapamiętanie stanu }
    FCursorInSquare := LInSquare;
    { przemalowanie samego kwadratu }
    DrawSquare();
  end;
end;

Metoda wyżej działa w ten sposób, że najpierw sprawdza, czy kursor znajduje się nad kwadratem i ten stan wpisuje do lokalnej zmiennej; Jeśli wartość zmiennej LInSquare jest inna niż wartość pola FCursorInSquare, to znaczy, że użytkownik najechał kursorem nad kwadrat lub zabrał go znad jego obszaru; Jeśli faktycznie tak się stało to wartość zmiennej zostaje wpisana do pola (aby zapamiętać nowy stan) oraz przemalowany zostaje sam kwadracik, za pomocą metody DrawSquare;

Dzięki takiemu zabiegowi, warunek porównujący stary stan z bieżącym wyklucza przemalowywanie komponentu, jeśli tego nie potrzebujemy; Czyli kontrolka nie zostanie przemalowana, jeśli użytkownik przesuwa kursor po tle kontrolki lub wewnątrz kwadratu;

W metodzie WMMouseMove możesz wykonać jeszcze inne czynności, takie jak np. zmiana kursora; Do tego celu nalezy wykorzystać właściwość Screen.Cursor; Przykład metody z dodaną zmianą kursora:

procedure TMyControl.WMMouseMove(var AMessage: TLMMouseMove);
var
  LInSquare: Boolean;
begin
  inherited WMMouseMove(AMessage);
  LInSquare := PtInRect(FSquareRect, SmallPointToPoint(AMessage.Pos));
  
  if FCursorInSquare <> LInSquare then
  begin
    FCursorInSquare := LInSquare;
    DrawSquare();
    
    { zmiana kursora }
    Screen.Cursor := IfThen(FCursorInSquare, crHandPoint, Self.Cursor);
  end;
end;

Kod zmiany kursora znajduje się wewnątrz warunku, więc wykonywany jest tylko wtedy, gdy użytkownik umieścił kursor nad kwadratem lub zabrał go znad jego powierzchni; Jeśli kursor jest nad kwadratem, ustawiony zostaje kursor crHandPoint, czyli łapka z wysuniętym palcem (z reguły, bo zależy to od tego, co mamy ustawione w systemie); A jeśli kursor zabrano znad kwadratu, zostaje on przywrócony do wartości z właściwości Cursor naszej kontrolki;

Całość pisałem z pamięci, więc powinno działać prawidłowo;


Dodam tylko, że praktycznie taką samą metodę wykorzystuję w swoim formatowanym labelku, do przemalowywania linków w nim się znajdujących:

procedure TFormatLabel.WMMouseMove(var AMessage: TLMMouseMove);
var
  LLinkIdx: Integer;
begin
  inherited WMMouseMove(AMessage);

  if InterfaceActive then
  begin
    LLinkIdx := LinkIndexAtCursorPosition(AMessage.Pos.ToPoint());

    with FLinksMap do
    begin
      if LinkHovered then
      begin
        if HoveredLinkIndex <> LLinkIdx then
        begin
          DoLinkLeave();
          HoveredLinkIndex := LLinkIdx;

          if LinkHovered then
            DoLinkEnter();
        end;
      end
      else
        if HoveredLinkIndex <> LLinkIdx then
        begin
          HoveredLinkIndex := LLinkIdx;
          DoLinkEnter();
        end;
    end;
  end;
end;

Metoda LinkIndexAtCursorPosition pobiera indeks linku, znajdującego się pod kursorem;

Struktura AMessage.Pos nie posiada metody ToPoint - jest to ręcznie zdefiniowana metoda za pomocą helpera; Nic innego jak cukier składniowy, zastępujący wywoływanie metody SmallPointToPoint.
Jest to dużo bardziej skomplikowane, bo dodatkowo obsługiwane są własne hinty, zmiana kursora, przemalowywanie linku(ów) itd.; Jednak kod jest napisany w sposób odpowiedni (optymalny), dlatego też ani linki, ani cały komponent nie mrugają na ekranie.

0

Na sprawdzenie warunku czy jest zmiana wpadłem, ale nie wiem co mi strzeliło do głowy bo zrobiłem straszną głupotę. Mniej więcej tak:

FCursorInSquare := LInSquare;
if FCursorInSquare <> LInSquare then
  begin
    DrawSquare();
  end;
 

Teraz jeszcze jedno pytanie. Ja połączyłem obsługę tego zdarzenia z MouseLeave. Czy jest to prawidłowe? W końcu jeśli wyjadę za komponent to już nie będzie to zdarzenie MouseMove, więc nie przemaluje się na powrót. Ale może jest jakaś możliwość żeby to zrobić.

0

Jeśli najedziesz kursorem nad komponent to zostają wysłane dwa komunikatu - CM_MOUSEENTER oraz od razu LM_MOUSEMOVE; Po zabraniu kursora tylko CM_MOUSELEAVE; To które komunikaty należy obsłużyć, zależy od konkretnego przypadku, bo możesz potrzebować oprogramować dwa (wymienione CM_*) lub wszystkie trzy;

  • komunikat CM_MOUSEENTER oprogramuj wtedy, gdy dany zestaw instrukcji ma być wykonany tylko raz - po wejściu kursora nad powierzchnię komponentu;
  • komunikat CM_MOUSELEAVE oprogramuj wtedy, gdy dane instrukcje mają być wykonane tylko raz - po zabraniu kursora znad kontrolki;
  • komunikat LM_MOUSEMOVE oprogramuj wtedy, gdy zestaw instrukcji ma być wykonywany wielokrotnie, podczas ruchu kursora we wnętrzu kontrolki;

Edit: Niestety komunikat CM_MOUSELEAVE nie jest wysyłany zawsze - problem występuje wtedy, gdy nasz komponent jest dosunięty do krawędzi formularza i bardzo szubko przesuniemy kursor znad kontrolki poza formularz; Owy formularz nie zdąży zarejestrować tego ruchu w całości, przez co stan kontrolki nie powróci do domyślnego;

Tutaj jest mój wątek, w którym @kAzek podał przykład dodania śledzenia myszy, w postaci krótkiego kodu korzystającego z Win32 API; Niestety nawet to nie pomagało, więc trzeba było problem obejść; Tym obejściem było dodanie timera, który pilnował kursor i w razie nieodebrania komunikatu CM_MOUSELEAVE, wysyłał go do kontrolki; Wtedy też timer był wyłączany, a kontrolka mogła już prawidłowo zareagować;

Nie wiem czy ten błąd w LCL nadal istnieje, więc pasuje to sprawdzić; Ja nie będę miał z tym problemu, bo wszystkie klasy które używają tego timera, mają kod jego obsługi objęty w dyrektywy {$IFDEF}; Jeśli bug został poprawiony to zaremuję specjalny symbol i tyle - nic więcej nie trzeba będzie robić, aby pozbyć się tego timera z kodu kontrolek.

0

To już chyba ostatnie pytanie z komponentów :D Dalej będę się doszkalał i kombinował z tym co wiem.

Mam cały czas ten sam komponent, na którym jest kilka napisów. Chciałbym teraz dodać obsługę zdarzenie kliknięcia myszą, na konkretny napis.

Wcześniej, zanim zacząłem tworzyć własny komponent po prostu nałożyłem kilka Labelów i do onClik przypisałem odpowiednią procedurę. Teraz tak jak mówię, chciałbym do każdego napisu dodać coś tak aby po naciśnięciu wykonywała się procedura. Tyle tylko, że procedura nie ma być deklarowana w kodzie komponentu, a już w trakcie pisania programu, a więc w momencie pisania komponentu nie wiem jaka procedura to będzie.

0

Zbierz kilku chłopa i przelejcie mi na piwo, a urządzimy sobie warsztaty on-line z programowania :D

Chciałbym teraz dodać obsługę zdarzenie kliknięcia myszą, na konkretny napis.

Z tym będzie nieco więcej roboty, ale prostej roboty; Najpierw musisz określić dane;

Załóżmy, że na kontrolce mają być trzy napisy, a komponent ma generować zdarzenie kliknięcia, po kliknięciu rzecz jasna (uprośćmy termin "kliknięcie" do obsługi komunikatu LM_LBUTTONUP, gdy kursor znajduje się nad którymś napisem);

Na temat każdego napisu trzeba przechowywać co najmniej dwie informacje - tekst oraz pozycję w kontrolce (najlepiej jako TRect); Te informacje możesz upublicznić, robiąc z nich właściwości w sekcji Published - dzięki temu będzie je można edytować w oknie Inspektora obiektów; Najlepiej będzie je wrzucić do jednej tablicy, później wyjaśnię dlaczego;

W konstruktorze klasy kontrolki, nadaj tym napisom domyślny tekst oraz współrzędne, a także za pomocą metod Canvas.TextWidth oraz Canvas.TextHeight uzupełnij odpowiednio pola Right i Bottom struktur typu TRect; Rekordy tego typu przydadzą się do sprawdzania czy kursor znajduje się nad danym napisem (za pomocą funkcji Types.PtInRect);

Następnie napisz sobie prywatną metodę, która na podstawie współrzędnych kursora zwróci indeks etykiety; Jeśli kursor znajduje się nad którymś napisem, funkcja zwraca jego indeks (licząc od 0 rzecz jasna), w przeciwnym razie zwraca -1; Pseudokod niżej:

function TMyPanel.LabelIndexAtCursorPos(ACursorPos: TPoint): Integer;
var
  LLabelIdx: Integer;
begin
  for LLabelIdx := 0 to High(FLabels) do
    if PtInRect(FLabels[LLabelIdx].Area, ACursorPos) then
      Exit(LLabelIdx);
      
  Result := -1;
end;

W tym przykładzie, FLabels to zwykła macierz rekordów, zawierających tekst labelka w hipotetycznym polu Text oraz jego współrzędne (powierzchnię) w polu Area; W metodzie WMMouseMove sprawdzamy pozycję kursora i wykonujemy odpowiednią akcję, czyli przemalowanie napisu, zmiana kursora itd. - to opisywałem wcześniej;

Obsługa kliknięcia może się odbywać na wiele sposobów; Do poprawnego przemalowywania napisów podczas ruchu kursora, potrzebne będzie pole przechowujące indeks aktywnego napisu; Ten indeks należy ustawiać w metodzie WMMouseMove i odpowiednio reagować na trzy sytuacje:

  • najechano z pustej przestrzeni komponentu na napis,
  • najechano z jednego napisu na drugi (czyli zjechano z jednego i najechano na drugi),
  • zjechano z napisu na pustą przestrzeń komponentu;
    We wcześniejszym poście podałem kod metody WMMouseMove mojego komponentu, w którym akcje najechania i zjechania obsługiwane są przez metody DoLinkEnter i DoLinkLeave, więc skorzystaj z podanej tam drabinki **If**ów; Tamten kod jest praktycznie tym, czego w swoim komponencie potrzebujesz;

Jeśli w metodzie WMMouseMove zapamiętasz indeks napisu np. w polu o nazwie FHoveredLabelIdx, to kod metody WMLButtonUp będzie bajecznie prosty; Wystarczy wywołać odpowiednie zdarzenie lub zdarzenie z odpowiednim parametrem; Sugeruję stworzenie jednego zdarzenia, w którym przekazywany będzie indeks klikniętego napisu; Przykład:

type
  TMyPanelLabelClick = procedure(ASender: TObject; ALabelIndex: Integer) of object;


private
  FHoveredLabelIdx: Integer;
  FOnLabelClick: TMyPanelLabelClick;
private
  procedure DoLabelClick();
protected
  procedure WMLButtonUp(var AMessage: TLMLButtonUp); message LM_LBUTTONUP;
published
  property OnLabelClick: TMyPanelLabelClick read FOnLabelClick write FOnLabelClick;


procedure TMyPanel.DoLabelClick(ALabelIndex: Integer);
begin
  if Assigned(FOnLabelClick) then
    FOnLabelClick(Self, FHoveredLabelIdx);
end;

procedure TMyPanel.WMLButtonUp(var AMessage: TLMLButtonUp);
begin
  inherited WMLButtonUp(AMessage);
  
  if FHoveredLabelIdx <> -1 then
    DoLabelClick();
end;

1 użytkowników online, w tym zalogowanych: 0, gości: 1