Błędne obliczanie pozycji komponentu na podstawie zawartości właściwości Anchors

0

Użerania się z komponentem ciąg dalszy... Tym razem problem jest bardziej zawiły i dotyczy póki co Delphi7;

Dotyczy on dziedziczonej właściwości Anchors i dostosowywania swojego rozmiaru w przedefiniowanej metodzie SetBounds, gdy zbiór ten zawiera poniższe wartości:

Anchors = [akBottom, akLeft, akRight];

Podczas każdorazowej zmiany rozmiaru sprawdzane jest, czy jego szerokość uległa zmianie; Jeżeli tak - odpowiednia metoda "mierzy" zawartość i zwraca nową wysokość; Jeżeli nowa wysokość różni się od poprzedniej, modyfikowana jest zawartość parametru ATop metody SetBounds;

Jeżeli dobrze liczę, to różnicę poprzedniej i nowej wysokości trzeba dodać do poprzedniej wysokości; Czyli:

ATop := Self.Top + (ATop - Self.Top);

I to by załatwiało sprawę, ale tylko teoretycznie... Jeżeli uruchomię program i porozciągam formularz jedynie w pionie, to wszystko gra; Rozciąganie tylko w poziomie działa dobrze, ale głupieje randomowo i nie mogę dojść do tego, dlaczego nie wiadomo skąd Top komponentu zostaje zmieniony na zupełnie inny, niż to wynika z obliczeń; Ba, zostaje randomowo zmieniony nawet wtedy, gdy szerokość komponentu się nie zmienia, czyli obliczenia nowej wysokości i odstępu od góry są pomijane;

Wykonałem testowy komponent, implementując jedynie to co nie działa prawidłowo, czyli metodę SetBounds i właściwość Anchors; Kod komponentu poniżej:

type
  TTestGraphControl = class(TGraphicControl)
  protected
    procedure Paint(); override;
  public
    procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
  published
    property Anchors;
  end;

{...}

// metodę Paint pomijam, bo nie dotyczy problemu

procedure TTestGraphControl.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);

  { symulacja metody obliczającej nową wysokość komponentu }
  function SetNewHeigh(): Integer;
  begin
    if Abs(Self.Width - AWidth) > 25 then
      Result := Self.Height + (Self.Width - AWidth)
    else
      Result := AHeight;
  end;

begin
  { jeżeli szerokość komponentu się zmieniła }
  if Self.Width <> AWidth then
  begin
    { jeżeli w zbiorze Anchors nie znajdują się razem flagi akTop i akBottom }
    if (akBottom in Anchors) and not (akTop in Anchors) then
    begin
      { symulacja metody obliczającej wysokość komponentu na podstawie jego zawartości }
      AHeight := SetNewHeigh();

      { jeśli wysokość komponentu się zmieniła - zwiększamy odpowiednio odstęp od góry,
        obliczając różnicę starej i nowej wysokości i dodając ją do obecnego odstępu }
      if Self.Height <> AHeight then
        ATop := Self.Top + (Self.Height - AHeight);
    end;
  end;

  { wywołanie bazowej metody, podając zmodyfikowane parametry }
  inherited SetBounds(ALeft, ATop, AWidth, AHeight);
end;

Jak widać w powyższym kodzie, nowa wysokość komponentu i jego odstęp od góry obliczane są jedynie w przypadku, gdy zmieniła się szerokość komponentu, oraz gdy Anchors zawiera flagę akBottom i jednocześnie nie zawiera flagi akTop; Niestety ta metoda coś wariuje, źle i w złych momentach oblicza odstęp od góry; Niestety nie mogę namierzyć kodu, który to powoduje; To obliczanie nowej wysokości to tylko symulacja - wysokość komponentu nie zawsze zmienia się, jeżeli zmieniła się jego szerokość, stąd ten przykładowy warunek;

Objawy poniżej:

forms.png

Co ciekawe, jeżeli szerokość komponentu nie zmieniła się, czyli wszystkie obliczenia i modyfikacje parametrów metody SetBounds są pomijane, komponent i tak zmienia swój offset od góry (wartość ATop); Widać to zaraz po uruchomieniu programu, gdy rozciągam formularz jedynie w pionie; Obliczenia są pomijane, więc dlaczego komponent zmienia swoje położenie?

No właśnie - sprawdziłem kod klasy TControl; W bazowej metodzie SetBounds wywołane zostają metody UpdateAnchorRules, RequestAlign i Resize, które najwyraźniej coś jeszcze działają na rozmiarach i położeniu komponentu, psując moje obliczenia; Niestety nie mogę pominąć wywołania inherited SetBounds, dlatego że inaczej nie zmienię rozmiarów komponentu, a nawet jeśli zmieniałbym bezpośrednio właściwości Left, Top, Width czy Height komponentu, to stworzę bombę rekurencyjną powodującą StackOverflow; I nic dziwnego, bo zmiana wartości którejkolwiek z wymienionych właściwości wywołuje SetBounds i kicha;

Czy ktoś ogarnia to w jaki sposób poprawnie ustawić położenie i rozmiar komponentu, bądź oszukać wewnętrzne SetBounds? Pałuję się z tym już drugi dzień i pomysły mi się skończyły... Najwidoczniej coś pominąłem, bo obliczenia wydają mi się poprawne;

Do posta dołączam źródła komponentu oraz źródła aplikacji, która korzysta z tego komponentu; Aby odtworzyć buga, wystarczy się chwilę pobawić formularzem, rozciągając go w różnych kierunkach; W końcu odstęp dołu komponentu od dołu formularza będzie zupełnie inny, niż zaraz po uruchomieniu programu;

Pamiętajcie, że chodzi o Delphi7; Dziękuję z góry za pomoc.

1

http://docwiki.embarcadero.com/Libraries/XE2/en/Vcl.Controls.TControl.SetBounds

Calling SetBounds does not necessarily result in the Left, Top, Width, and Height properties changing to the specified values. Before the properties are changed, the AutoSize or Constraints property may limit the changes, and an OnCanResize (or OnConstrainedResize) event handler may change the new values. After the control's Left, Top, Width, and Height properties are changed, SetBounds generates an OnResize event.

Note: Component writers can change the Left, Top, Width, and Height properties while bypassing all resize events and constraint or autosize logic by using the UpdateBoundsRect method instead.
0

@mca64 - owszem, czytałem i o tym w pomocy Delphi7; Tyle że jeżeli użyję UpdateBoundsRect zamiast SetBounds - komponent całkowicie świruje, dlatego próbowałem czegoś innego; Problem jest nawet wtedy, gdy w ogóle nie zmieniam sam rozmiarów, czyli używam niezmienionych parametrów metody SetBounds;


Jedyne co potrzebuję to poprawnie ustawić ten Top, ale nie pomijając wewnętrznej obsługi wyrównania i dostosowywania rozmiaru, bo to jest potrzebne; Potrzebuję raczej triku, który tak ustawi ATop, że rozmiar i położenie komponentu będzie prawidłowe (takie jak wychodzi z normlanych obliczeń) po dodatkowych wewnętrznych zabiegach metody SetBounds;

To coś w rodzaju kasowania ramki focusa w ListBox - ona i tak zostanie namalowana, ale jeżeli sami ją namalujemy, to kolejne malowanie ramki spowoduje taki efekt, jakby jej w ogóle nie było (kolor dla FrameRect jest xorem Pena, więc drugie jej namalowanie po prostu ją "ukryje"); Nad czymś takim myślę, tyle że z oszukaniem Topu komponentu, skoro zwykłe wywołanie SetBounds bruździ, a jego pomnięcie upośledzi komponent;

A jeżeli się to nie uda, to po prostu zrezygnuję z tej zabawy i nie będzie automatycznego dostosowywania wysokości komponentu z jednoczesnym wyrównaniem do lewej, prawej i dołu (bez góry).

0

No dobra - zakasałem rękawy i w końcu udało mi się sensownie napisać tę metodę (SetBounds); Kod ostatecznej i poprawnej wersji metody poniżej:

procedure TFuriousLabel.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
begin
  if Self.Width <> AWidth then
  begin
    if (Constraints.MaxWidth <> 0) and (AWidth > Constraints.MaxWidth) then
      AWidth := Constraints.MaxWidth
    else
      if (Constraints.MinWidth <> 0) and (AWidth < Constraints.MinWidth) then
        AWidth := Constraints.MinWidth;

    FDimensions^.X := AWidth;
    // wywołanie tokenizera, budującego mapę tokenów na podstawie nowej szerokości komponentu
    // ustawia on nową wysokość zawartości, czyli wartość pola FDimensions^.Y
    ContentChanged(True);
  end;

  if FAutoFitHeight and (AHeight <> FDimensions^.Y) then
    AHeight := FDimensions^.Y;

  inherited SetBounds(ALeft, ATop, AWidth, AHeight);
end;

Przy okazji podczepiłem MinWidth i MaxWidth z właściwości Constraints - razem wszystko działa elegancko, więc nie musze kombinować nic z UpdateBoundsRect; No i oczywiście kombinowania z wywołaniem tych metod, które SetBounds posiada, a UpdateBoundsRect już nie; Dlatego wcześniej pisałem o ułomności, której pozbycie się raczej nie było możliwe (przynajmniej w prosty sposób);

Jak widać w powyższym kodzie nie modyfikuję wartości parametru ATop - ona musi pozostać bez zmian, przynajmniej w metodzie SetBounds; Sprawdziłem to jak ma się zachowywać komponent i jeżeli ATop zostawię w spokoju, to komponent będzie się poprawnie dopasowywał do otoczenia, nawet jeśli zbiór Anchors zawiera enumy [akTop, akLeft, akRight];

Tak więc problem rozwiązany - teraz pozostało przeportowanie kodu dla Lazarusa i fajrant; A jest to konieczne, bo z kolei SetBounds w LCL w ogóle nie uwzględnia kotwic...

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