Lab 8 - To i owo — IPP 2011/12

Spis treści

Poprzedni temat

Lab 7 - MVC

Następny temat

Lab 9

Lab 8 - To i owo

Garść niezbyt zorganizowanych informacji, przydatnych przy zadaniu zaliczeniowym.

Tworzenie komponentów

Dodawanie kontrolek w kodzie programu

Graficzny edytor Lazarusa pozwala łatwo rozmieścić kontrolki w oknach tworzonej aplikacji. Są jednak sytuacje, w których nie można z góry przewidzieć liczby czy rozmieszczenia niektórych elementów interfejsu:

  • Liczba kontrolek może zależeć od danych wprowadzonych przez użytkownika lub wczytanych z pliku. Może też ulegać zmianie w trakcie działania aplikacji. Na przykład edytor diagramów encji może mieć osobny komponent dla każdej wyświetlanej na diagramie encji.
  • Niektóre aplikacje obsługują kilka schematów rozmieszczenia kontrolek. Schemat wybierany jest na podstawie preferencji użytkownika, rozmiaru ekranu itp.
  • Dodanie własnej kontrolki do palety Lazarusa jest możliwe, ale bywa uciążliwe.
  • ...

Tworzenie kontrolki w trakcie działania programu można podzielić na kilka etapów:

Stworzenie obiektu

Najpierw trzeba stworzyć obiekt odpowiadający kontrolce. W tym celu należy wywołać konstruktor stosownej klasy. Poniższy przykład ilustruje tworzenie przycisku

ExitButton : TButton;

...

ExitButton := TButton.Create(Self);

Konstruktor klasy TButton ma jeden parametr - obiekt, który stanie się właścicielem nowo utworzonej kontrolki. Mechanizm ten służy do zarządzania pamięcią - gdy jakaś kontrolka jest zwalniana, dealokacji ulegają wszystkie obiekty do niej należące. Jako właściciela można tez podać nil - wtedy nowy komponent musi zostać zwolniony przez wywołanie Free.

Ostrzeżenie

Jeśli kontrolka ma właściciela, nie jest możliwe zwolnienie jej poprzez wywołanie Free - przy zwalnianiu właściciela dojdzie wówczas do podwójnej dealokacji, co może mieć trudne do przewidzenia konsekwencje.

Rozmieszczenie

Po stworzeniu kontrolki trzeba ustawić atrybuty odpowiedzialne za jej położenie w okienku. Najważniejszy z nich to Parent - określa, w której kontrolce należy zagnieździć nowo utworzony komponent.

ExitButton.Parent := MainPanel;

Rozmieszczenie komponentów można ustalić na kilka sposobów

  • Ustawiając własności Top, Left, Width oraz Height na odpowiednie wartości, wyrażone w pikselach. Zamiast ręcznie wyliczać wysokość i szerokość można ustawić własność AutoSize - rozmiar dopasuje się wtedy do zawartości kontrolki.

    ExitButton.Left := 0;
    ExitButton.Top := 0;
    ExitButton.AutoSize := True;
    

    Taki sposób rozmieszczenia kontrolek jest bardzo mało elastyczny - nie uwzględnia m.in. zmian rozmiaru okienka czy różnic w wielkości innych kontrolek, wynikających z dostępności czcionek bądź charakterystyki systemu okienkowego.

  • Własność Align pozwala na ‘przyklejenie’ komponentu do wybranych krawędzi zawierającej go kontrolki. Na przykład ustawienie jej na alClient spowoduje zajęcie calej dostępnej przestrzeni.

  • Własność Anchors pozwala na bardziej precyzyjne ustalenie położenia wszystkich komponentu względem innych kontrolek. Poniższy przykład ilustruje sposób rozmieszczenia kontrolki edycyjnej i napisu w taki sposób, by

    • Napis znajdował się z prawej strony, w odległości 10px od edytora.
    • Odległość od prawej i lewej krawędzi zewnętrznego panelu wynosiła 15px.
    • Kontrolki były wycentrowane w pionie.
    • Rozmiar napisu był stały, a resztę przestrzeni zajmował edytor.
    FLabel.AnchorSide[akTop].Side := asrCenter;
    FLabel.AnchorSide[akTop].Control := Self;
    FLabel.AnchorSide[akRight].Side := asrRight;
    FLabel.AnchorSide[akRight].Control := Self;
    FLabel.Anchors := [akRight, akTop];
    FLabel.BorderSpacing.Right := 15;
    
    FEdit.AnchorSide[akTop].Side := asrCenter;
    FEdit.AnchorSide[akTop].Control := FLabel;
    FEdit.AnchorSide[akLeft].Side := asrLeft;
    FEdit.AnchorSide[akLeft].Control := Self;
    FEdit.AnchorSide[akRight].Side := asrLeft;
    FEdit.AnchorSide[akRight].Control := FLabel;
    FEdit.Anchors := [akLeft, akTop, akRight];
    FEdit.BorderSpacing.Left := 15;
    FEdit.BorderSpacing.Right := 10;
    

    Konfiguracja dla każdej krawędzi składa się z kilku elementów:

    • Ustawienia AnchorSide[].Control, mówiącego, względem której kontrolki będzie pozycjonowana krawędź

    • AnchorSide[].Side wskazuje, do której krawędzi ustawionej wcześniej kontrolki się odnosimy.

    • Struktura BorderSpacing pozwala na ustalenie odległości (w pikselach).

      Ostrzeżenie

      Wyrażenie tej wartości bezpośrednio w pikselach nie jest zbyt dobrym pomysłem - trzeba wziąć pod uwagę różne rozmiary i rozdzielczości ekranu...

    • Zbiór Anchors mówi, które krawędzi kontrolek maja ustawione ograniczenia.

  • Panele, okienka i inne zasobniki kontrolek maja atrybut ChildSizing, pozwalający rozmieścić zawarte w nich komponenty w rzędach lub kolumnach. Niestety możliwości ustalania rozmiaru i zachowania komórek tak utworzonej tabeli są bardzo ograniczone.

Dokładniejszy opis mechanizmów rozmieszczenia kontrolek w Lazarusie można znaleźć na stosownej stronie.

Ustawienie własności i zdarzeń ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Po stworzeniu kontrolki można zmienić jej własności (np. wyswyświetlany napis czy kolor). Można tez ustawić kod obsługujący zdarzenia. Zdarzenia to własności, których wartościami są wskaźniki do metod. Kod ustawiający obsługę zdarzenia może wyglądać np. tak:

procedure TMainForm.ExitButtonClick(Sender: TObject);
begin
  Close;
end;

...

  ExitButton.OnClick := @Self.ExitButtonClick;

Zwalnianie

Jeżeli kontrolka nie ma ustawionego właściciela, trzeba ja zwolnić przez wywołanie Free.

 procedure THelperFrame.FormDestroy(Sender: TObject);
 begin
   FreeAndNil(ExitButton);
   FreeAndNil(OtherButton);
end;

Nowe typy kontrolek

Komponenty to obiekty klas dziedziczących z TComponent. Stworzenie własnego typu komponentu sprowadza się więc do zaimplementowania stosownej podklasy. Zwykle nie będzie ona wyprowadzona bezpośrednio z TComponent - wygodniej jest skorzystać z bardziej specyficznych klas, które zawierają już część pożądanej funkcjonalnosci. Najczęściej stosowane są

  • TLabel, TPanel, ... - jeśli naszym celem jest dodanie jakiejś funkcji do istniejącej kontrolki, dobrym pomysłem może być rozszerzenie jej klasy.
  • TCustomLabel, TCustomPanel, ... - czasami potrzebne jest również ukrycie pewnych funkcji czy własności Ułatwia to ma konwencja, polegająca na tworzeniu dwóch klas dla każdej kontrolki. Pierwsza, z prefiksem Custom, definiuje wszystkie cechy kontrolki, ale udostępnia je tylko podklasom (tz. są zadeklarowane jako chronione). Druga klasa rozszerza pierwsza, odsłaniając jej własności Wyprowadzając komponent z klasy TCustom... możemy ukryć wybrane funkcje bez konieczności ponownej implementacji calej kontrolki. Niestety wspomniana konwencja nie jest zbyt sciśle przestrzegana przez bibliotekę standardowa.
  • TWinControl - to nadklasa kontrolek, które zawierają inne kontrolki i/lub mogą być aktywne (tz. otrzymywać wejście z klawiatury).
  • TGraphicControl - klasa ‘lekkich’ komponentów, które nie opanowują kontrolek z poziomu systemu okienkowego. Nie mogą zawierać innych kontrolek. Kod odpowiedzialny za ich wyświetlanie umieszcza się w metodzie Paint.
  • TCustomControl - podklasa TWinControl, która pozwala na dodanie własnego kodu do procedur odpowiedzialnych za wyświetlanie kontrolki (Paint).
  • TComponent - bezpośrednio z tej klasy dziedziczą zwykle komponenty, które nie maja graficznej reprezentacji, takie jak np. TOpenDialog, TTimer czy TApplicationProperties. Opakowanie takich obiektów w komponenty pozwala wygodnie ustawiać ich własności w interfejsie Lazarusa. Umożliwia tez wykorzystanie komponentowych mechanizmów zarządzania pamięcią (obiekty zostają automatycznie zwolnione wraz z właścicielem).

Przykład - kontrolka z kodem wyświetlającym

Pierwszym przykładem jest kontrolka wyświetlająca prosty, poziomy gradient - może być wykorzystana do dekoracji różnych elementów interfejsu. Komponent nie zawiera innych kontrolek i nie obsługuje wejścia z klawiatury, więc jego klasa bazowa będzie TGraphicControl.

  • Komponent ma dwie własności: kolor lewej i prawej strony gradientu. Obie są zadeklarowane jako published, co umożliwia edytowanie ich wartości przez stosowne okienko Lazarusa.
  • Procedury ustawiające wartość są chronione i wirtualne - dzięki temu podklasy mogą reagować na zmiany wartości
  • Dyrektywa default przy deklaracji własności nie powoduje przypisania żadnej wartości - trzeba to zrobić w konstruktorze. Wartość default ma znaczenie przy serializacji (tz. zapisywaniu stanu kontrolki do pliku *.lfm) - jeśli wartość atrybutu nie różni się od zadeklarowanej, nie jest zapisywana.
  • Rysowanie odbywa się w procedure Paint.
  • Po każdej zmianie stanu komponentu, która może wpływać na jego wygląd, należy wywołać Invalidate. Takie wywołanie występuje np. w metodach Set....
  • Procedura Register jest wykorzystywana przy dodawaniu nowej kontroli do palety Lazarusa/
  unit nicebox;

  {$mode objfpc}{$H+}

  interface

  uses
    Classes, SysUtils, Controls, Graphics;

  type
    TNiceBox = class(TGraphicControl)
    private
      FStartColor, FEndColor : TColor;
    protected
      procedure SetStartColor(NewStartColor
: TColor); virtual;
      procedure SetEndColor(NewEndColor : TColor); virtual;

      procedure Paint; override;
    public
      constructor Create(TheOwner : TComponent); override;
    published
      property StartColor : TColor
        read FStartColor write SetStartColor default clWhite;
      property EndColor : TColor
        read FEndColor write SetEndColor default clBlack;
    end;

  procedure Register;

  implementation

  constructor TNiceBox.Create(TheOwner : TComponent);
  begin
    inherited Create(TheOwner);
    FStartColor := clWhite;
    FEndColor := clBlack;
  end;

  procedure TNiceBox.SetStartColor(NewStartColor: TColor);
  begin
    if NewStartColor <> FStartColor then
    begin
      FStartColor := NewStartColor;
      Invalidate;
    end;
  end;

  procedure TNiceBox.SetEndColor(NewEndColor: TColor);
  begin
    if NewEndColor <> FEndColor then
    begin
      FEndColor := NewEndColor;
      Invalidate;
    end;
  end;

  procedure TNiceBox.Paint;
  begin
    inherited Paint;
    Canvas.GradientFill(ClientRect, FStartColor, FEndColor, gdHorizontal);
  end;

  procedure Register;
  begin
    RegisterComponents('Additional',[TNiceBox]);
  end;

  end.

Tak wygląda formularz z przykładową kontrolka:

_images/lab08scr02.png

Jak widać na poniższym obrazku, Lazarus potrafi wykorzystać swój wbudowany edytor kolorów przy wyświetlaniu nowych własności:

_images/lab08scr03.png

Przykład - kontrolka złożona

Następny przykład to kontrolka wyświetlająca punktowy wynik rywalizacji miedzy dwiema drużynami Składa się z trzech napisów (tz. obiektów TLabel). Klasa bazowa jest TWinControl. Dodatkowo kontrolka definiuje zdarzenie - kliknięcie w znak oddzielający wyniki.

  • Definicja zdarzenia składa się z trzech elementów

    • Pola FOnSeparatorClick, przechowującego procedurę obsługi zdarzenia. Typ tego pola (TNotifyEvent) jest zadeklarowany w bibliotece standardowej jako

      TNotifyEvent = procedure (Sender: TObject) of object;
      
    • Własności OnSeparatorClick, umożliwiającej publiczny dostęp do wspomnianego pola.

    • Procedury chronionej DoSeparatorClick, wywoływanej przez komponent, gdy zajdzie omawiane zdarzenie. Procedura sprawdza, czy ustawiono kod obsługi zdarzenia i, jeśli tak, wykonuje go.

  • Metoda CalculatePreferredSize pozwala wyliczyć domyślne rozmiary komponentu.

  • W konstruktorze widać przykład rozmieszczenia kontrolek przez mechanizmy omówione we wcześniejszej części notatek.

  • W praktyce taki komponent powinien mieć więcej własności - np. pozwalać na zmianę czcionki itp.

  unit scoreview;

  {$mode objfpc}{$H+}

  interface

  uses
    Classes, SysUtils, Controls, StdCtrls, ExtCtrls;

  type

    { TScoreView }

    TScoreView = class(TWinControl)
    private
      FLeftScore, FRightScore : Integer;
      FLeftLabel, FSepLabel, FRightLabel : TLabel;
      FOnSeparatorClick : TNotifyEvent;
      procedure HandleSepClick(Sender : TObject);
      procedure SetLabelTexts;
    protected
      procedure SetLeftScore(Score : Integer); virtual;
      procedure SetRightScore(Score : Integer); virtual;
      procedure DoSeparatorClick; virtual;
      procedure CalculatePreferredSize(
        var PreferredWidth, PreferredHeight: integer;
        WithThemeSpace: Boolean); override;
    public
      constructor Create(TheOwner : TComponent); override;
    published
      { Properties }
      property LeftScore : Integer read FLeftScore write SetLeftScore;
      property RightScore : Integer read FRightScore write SetRightScore;

      { Events }
      property OnSeparatorClick : TNotifyEvent read FOnSeparatorClick
                                               write FOnSeparatorClick;
    end;

  procedure Register;

  implementation

  constructor TScoreView.Create(TheOwner: TComponent);
  begin
    inherited Create(TheOwner);

    FLeftScore := 0;
    FRightScore := 0;

    FLeftLabel := TLabel.Create(Self);
    FLeftLabel.Parent := Self;
    FLeftLabel.AutoSize := True;

    FRightLabel := TLabel.Create(Self);
    FRightLabel.Parent := Self;
    FRightLabel.AutoSize := True;

    FSepLabel := TLabel.Create(Self);
    FSepLabel.Parent := Self;
    FRightLabel.AutoSize := True;

    FSepLabel.AnchorSide[akLeft].Side := asrCenter;
    FSepLabel.AnchorSide[akLeft].Control := Self;
    FSepLabel.AnchorSide[akTop].Side := asrCenter;
    FSepLabel.AnchorSide[akTop].Control := Self;
    FSepLabel.Anchors := [akLeft, akTop];

    FLeftLabel.AnchorSide[akRight].Side := asrLeft;
    FLeftLabel.AnchorSide[akRight].Control := FSepLabel;
    FLeftLabel.AnchorSide[akTop].Side := asrCenter;
    FLeftLabel.AnchorSide[akTop].Control := Self;
    FLeftLabel.Anchors := [akRight, akTop];

    FRightLabel.AnchorSide[akLeft].Side := asrRight;
    FRightLabel.AnchorSide[akLeft].Control := FSepLabel;
    FRightLabel.AnchorSide[akTop].Side := asrCenter;
    FRightLabel.AnchorSide[akTop].Control := Self;
    FRightLabel.Anchors := [akLeft, akTop];

    SetLabelTexts;
  end;

  procedure TScoreView.SetLabelTexts;
  begin
    FLeftLabel.Caption := IntToStr(FLeftScore);
    FRightLabel.Caption := IntToStr(FRightScore);
    if FLeftScore > FrightScore then
    begin
      FSepLabel.Caption := '>';
    end else if FLeftSCore = FRightScore then
    begin
      FSepLabel.Caption := ':';
    end else begin
      FSepLabel.Caption := '<';
    end;
  end;

  procedure TScoreView.HandleSepClick(Sender: TObject);
  begin
    DoSeparatorClick;
  end;

  procedure TScoreView.SetLeftScore(Score: Integer);
  begin
    if Score <> FLeftScore then
    begin
      FLeftScore := Score;
      SetLabelTexts;
    end;
  end;

  procedure TScoreView.SetRightScore(Score: Integer);
  begin
    if Score <> FRightScore then
    begin
      FRightScore := Score;
      SetLabelTexts;
    end;
  end;

  procedure TScoreView.DoSeparatorClick;
  begin
    if Assigned(FOnSeparatorClick) then
    begin
      FOnSeparatorClick(Self);
    end;
  end;

  procedure TScoreView.CalculatePreferredSize(var PreferredWidth,
    PreferredHeight: integer; WithThemeSpace: Boolean);
  begin
    PreferredHeight := FLeftLabel.Height;
    PreferredWidth := FLeftLabel.Width + FSepLabel.Width + FRightLabel.Width;
  end;

  procedure Register;
  begin
    RegisterComponents('Additional',[TScoreView]);
  end;

  end.

Lazarus odróznia zdarzenia od własności na podstawie typu - na ponizszym obrazku wdiac, ze atrybut ``OnSeparatorClick`` został poprawnie zaklasyfikowany:
_images/lab08scr04.png

Przykład - złożona kontrolka z dodatkowym kodem wyświetlającym

Ostatni przykład to połączenie kontrolki złożonej z dodatkowym kodem wyświetlającym Przedstawiona tu klasa to wariant panelu (tz. TPanel), w którym kolejne składowe są połączone strzałkami

  • Strzałki są:

    • Rysowane przez procedurę DrawArrow.
    • Odsunięte od punktu początkowego i docelowego o stały offset (ARROW_OFFSET).
    • Zakończone ostrzem o długości TIP_LENGTH i szerokości 2 * TIP_HEIGHT.
  • Punkty zaczepienia strzałek są wybierane w funkcji ChooseConnectionPoints. Mechanizm wyboru jest następujący

    • Zakładamy, ze dla każdej kontrolki mamy niepusta listę potencjalnych punktów zaczepienia, liczona w funkcji GetConnectionPoints. Tutaj przyjmujemy, ze owe punkty to środki boków prostokątnej otoczki każdego komponentu.
    • Analizujemy każdą parę punktów zaczepienia i wybieramy najblizsza, w miare mozliwosci gwarantujac, ze strzalka nie przetnie zadnego z komponentów. W tym przykładzie zawsze znajdzie się taka para, ale przy bardziej złożonych kształtach nie musi to być prawda.

    Ostrzeżenie

    Przy sprawdzaniu przecięć wszystkie kształty uznajemy za otwarte, tz. nie zawierające krawędzi

  unit strangepanel;

  {$mode objfpc}{$H+}

  interface

  uses
    Classes, SysUtils, Controls, ExtCtrls, Graphics;

  type
    TStrangePanel = class(TPanel)
    protected
      procedure Paint; override;
    end;

  implementation

  const
    ARROW_OFFSET = 10;
    TIP_LENGTH = 10;
    TIP_HEIGHT = 5;

  type
    TPoints = array of TPoint;

  function Rotate(P : TPoint; Cosinus, Sinus : Double) : TPoint;
  var
    X, Y : Double;
  begin
    X := Cosinus * Double(P.X) - Sinus * Double(P.Y);
    Y := Sinus * Double(P.X) + Cosinus * Double(P.Y);
    Rotate := Point(Trunc(X), Trunc(Y));
  end;

  procedure DrawArrow(Canvas : TCanvas;
                      Source, Target : TPoint);
  var
    RelX, RelY, Len, Sinus, Cosinus : Double;
    Offset, SourceWithOffset, TargetWithOffset, LeftTip, RightTip : TPoint;
  begin
    if (Source.X = Target.X) and (Source.Y = Target.Y) then Exit;

    RelX := Target.X - Source.X;
    RelY := Target.Y - Source.Y;

    Len :=  Sqrt(Sqr(RelX) + Sqr(RelY));
    Sinus := RelY / Len;
    Cosinus := RelX / Len;

    Offset := Rotate(Point(ARROW_OFFSET, 0), Cosinus, Sinus);
    SourceWithOffset := Point(Source.X + Offset.X, Source.Y + Offset.Y);
    TargetWithOffset := Point(Target.X - Offset.X, Target.Y - Offset.Y);

    LeftTip := Rotate(Point(-TIP_LENGTH, TIP_HEIGHT), Cosinus, Sinus);
    Inc(LeftTip.X, TargetWithOffset.X);
    Inc(LeftTip.Y, TargetWithOffset.Y);

    RightTip := Rotate(Point(-TIP_LENGTH, -TIP_HEIGHT), Cosinus, Sinus);
    Inc(RightTip.X, TargetWithOffset.X);
    Inc(RightTip.Y, TargetWithOffset.Y);

    Canvas.Line(SourceWithOffset, TargetWithOffset);
    Canvas.Line(TargetWithOffset, LeftTip);
    Canvas.Line(TargetWithOffset,
RightTip);
  end;

  function SqrDistance(P1, P2 : TPoint) : LongInt;
  begin
    SqrDistance := Sqr(P1.X - P2.X) + Sqr(P1.Y - P2.Y);
  end;

  function Product(P1, P2, P3 : TPoint) : Longint;
  begin
    Product  := (P2.X - P1.X) * (P3.Y - P1.Y) - (P3.X - P1.X) * (P2.Y - P1.Y);
  end;

  function Intersect(A1, A2, B1, B2 : TPoint) : Boolean;
  var
    D1, D2, D3, D4 : Longint;
  begin
    D1 := Product(A1, A2, B1);
    D2 := Product(A1, A2, B2);
    D3 := Product(B1, B2, A1);
    D4 := Product(B1, B2, A2);
    Intersect := (((D1 < 0) and (D2 > 0)) or ((D1 > 0) and (D2 < 0))) and
                 (((D3 < 0) and (D4 > 0)) or ((D3 > 0) and (D4 < 0))) ;
  end;

  function Intersect(P1, P2 : TPoint; R : TRect) : Boolean;
  var
    Found : Boolean;
  begin
    Found := False;
    Found := Found or Intersect(P1, P2, R.TopLeft, Point(R.Right, R.Top));
    Found := Found or Intersect(P1, P2, Point(R.Right, R.Top), R.BottomRight);
    Found := Found or Intersect(P1, P2, R.BottomRight, Point(R.Left, R.Bottom));
    Found := Found or Intersect(P1, P2, Point(R.Left, R.Bottom), R.TopLeft);
    Intersect := Found;
  end;

  function GetConnectionPoints(C : TControl) : TPoints;
  var
    Points : TPoints;
  begin
    SetLength(Points, 4);

    Points[0] := Point(C.Left + C.Width div 2, C.Top);
    Points[1] := Point(C.Left + C.Width, C.Top + C.Height div 2);
    Points[2] := Point(C.Left + C.Width div 2, C.Top + C.Height);
    Points[3] := Point(C.Left, C.Top + C.Height div 2);

    GetConnectionPoints := Points;
  end;

  procedure ChooseConnectionPoints(Source, Target : TControl;
                                   out SourcePoint, TargetPoint : TPoint);
  var
    SourcePoints, TargetPoints : TPoints;
    P1, P2 : TPoint;
    Distance, BestDistance : LongInt;
    I, J : Integer;
    Intersecting, FoundNonIntersecting : Boolean;
  begin
    SourcePoints := GetConnectionPoints(Source);
    TargetPoints := GetConnectionPoints(Target);
    BestDistance := MaxLongint;
    FoundNonIntersecting := False;

    for I := Low(SourcePoints) to High(SourcePoints) do
    begin
      for J := Low(TargetPoints) to High(TargetPoints) do
      begin
        P1 := SourcePoints[I];
        P2 := TargetPoints[J];
        Distance := SqrDistance(P1, P2);
        Intersecting := Intersect(P1, P2, Source.BoundsRect) or
                        Intersect(P1, P2, Target.BoundsRect);

        if (not Intersecting) and (not FoundnonIntersecting) then
        begin
          FoundNonIntersecting := True;
          BestDistance := Distance;
          SourcePoint := P1;
          TargetPoint := P2;
        end;

        if (Distance <= BestDistance) and
           (not (Intersecting and FoundNonIntersecting)) then
        begin
          BestDistance := Distance;
          SourcePoint := P1;
          TargetPoint := P2;
        end;
      end;
    end;
  end;

  procedure TStrangePanel.Paint;
  var
    I : Integer;
    ArrowSource, ArrowTarget : TPoint;
  begin
    inherited Paint;

    for I := 1 to ControlCount - 1 do
    begin
      ChooseConnectionPoints(Controls[I - 1], Controls[I],
                             ArrowSource, ArrowTarget);
      DrawArrow(Canvas, ArrowSource, ArrowTarget);
    end;
  end;

  end.

Tak wyglada przykladowa kontrolka, po dodaniu do niej kilku obiektów TShape:

_images/lab08scr01.png