niedziela, 22 marca 2009

Swing: Własny Layout Manager - wyższa szkoła jazdy

W poprzednim poście opisałem w krótkich słowach w jaki sposób stworzyć najłatwiej własny komponent graficzny w Swing. Nie pokusiłem się jednak o wyjaśnienie do czego taki komponent może się przydać, jakie są zalety jego użycia i po co marnować czas na tworzenie czegoś własnego w czasie gdy Sun Microsystems i wiele innych firm z chęcią za darmo dostarczy nam setki rozwiązań.

Po co więc wynajdywać na nowo koło, jednak w innym kolorze? Odpowiedź choć wydaje się głupia, jest niezwykle prosta: dla wygody! Mój punkt widzenia nie od razu zostanie podzielony przez ludzi, którym programowanie zdarzeniowe jest raczej obce. Aby to objaśnić najlepiej użyć przykładu. Jeśli tworzymy aplikację graficzną, to zapewne będzie obsługiwana przy pomocy klawiatury i myszy. Jeśli przechwytujemy zdarzenia z klawiatury, to jest to stosunkowo proste. Gorzej kiedy dochodzi już to drugie. Przyjmijmy, że mamy mapę świata. Program ma wyświetlić informacje na temat kilku największych miast świata. W momencie kliknięcia nad miastem i w pewnym promieniu pojawia się dymek z nazwą, współrzędnymi geograficznymi i populacją. Tak więc trzeba sprawdzić gdzie było kliknięcie. Można skorzystać z jednego komponentu z mapą, pobrać każde kliknięcie i w jego momencie współrzędne sprawdzić z listą miast "odpytując" każde o to, czy kliknięcie było na nim. Każde kliknięcie powoduje więc nawet tysiące dodatkowych obliczeń całkowicie zbędnych. Istnieje więc lepsze rozwiązanie. Każde miasto jako komponent graficzny na mapie. Gdy klikniemy w komponent system operacyjny przekaże to zdarzenie bezpośrednio do klikniętego komponentu. Nie musimy szukać klikniętego miasta, system operacyjny sam to zrobi za nas.

Mam nadzieję, że te spóźnione nieco objaśnienie wystarczy wszystkim. Czas przejść do spraw aktualnych. Tym razem zajmiemy się rozmieszczaniem elementów w komponencie graficznym, które także są komponentami. Technologia Swing korzysta z trzech różnych wzorców projektowych. Są to:
  • kompozyt,
  • obserwator,
  • MVC,
Ten ostatni znany jest z technologii webowych głównie, jednak to nie jedyne jego zastosowanie. Najistotniejszy jest dla nas w tej chwili ten pierwszy, choć sprawne oko zauważy każdy z nich w tym poście.

Przykład na objaśnienie przydatności komponentów jest nie bez znaczenia. MapLayout powstał na potrzeby aplikacji EloarsTracer służącej do wizualizacji trasy pakietów na mapie świata. Przedstawię go właśnie na przykładzie tej aplikacji.

W ostatniej opublikowanej wersji aplikacji Tracer (0.9.33) MapLayout posiada dość rozbudowaną strukturę i część zachowań BorderLayout'u. W tutorialu przedstawiony zostanie pierwotna wersja klasy MapLayout, bez powyższych rozszerzeń.

No to zaczynamy. Każdy Layout Manager musi implementować interface LayoutManager. Co ciekawe interface ten pochodzi z pakietu AWT. Jak można zauważyć wymaga on posiadania przez klasę kilku metod. Najważniejsza metoda to layoutContainer(Container parent), która odpowiada bezpośrednio za rozmieszczenie elementów.

Tworzymy klasę:
public class MapLayout implements LayoutManager
{
  layoutContainer(Container parent)
  {
  }
  addLayoutComponent(String name, Component comp) {}
  minimumLayoutSize(Container parent) {}
  preferredLayoutSize(Container parent) {}
  removeLayoutComponent(Component comp) {}
}

W zasadzie nie potrzebujemy konstruktora i destruktora dla naszej klasy, wystarczą nam metody implementowane z interface'u. Istniejący "nadzorcy rozmieszczenia" układają elementy według ich rozmiaru, kolejności dodania, czasem klucza i swoich parametrów. Ten jest od nich zupełnie inny. Potrzebuje do swojego działania znać pozycje równika i południka "0", rozmiar mapy w px, oraz współrzędne geograficzne punktu do umieszczenia na mapie. Współrzędne te diametralnie różnią się od współrzędnych (X,Y) komponentu graficznego na ekranie jak i na komponencie mapy. Z tego powodu do programu wprowadzony został interface MapElem. Implementujące go komponenty muszą umieć zwrócić swoje współrzędne geograficzne. Wszystko co nie implementuje tego interface'u zostanie pominięte przez klasę MapLayout.

Postać MapElem:
public interface MapElem
{
  public int getP();
  public int getR();
}

Jak widać jest to niezwykle prosty kod. P i R to oznaczają kolejno Południk i Równoleżnik. Nie jest istotne w jaki sposób informacje te zostaną pobrane z rozmieszczanego komponentu. Najlepiej, aby pochodziły z modelu danych i aby LayoutManager nie miał na nie wpływu. Jeśli w czasie rozmieszczania te dane się zmienią, z każdym ułożeniem element będzie się przesuwał pomimo, że ma stać w miejscu.

No to czas na metodę rozmieszczania elementów. Parametrem jej wywołania jest kontener, którego zawartość ma zostać rozmieszczona. Z niego pobierzemy jego rozmiar i wszystkie jego "dzieci", czyli jego zawartość.

layoutComponent:
public void layoutContainer(Container parent)
{
  int CompN=parent.getComponentCount();
  MapElem tmp=null;
  Component tmp2=null;
  int rown=parent.getHeight()/2;
  int polz=parent.getWidth()/2;
  for(int i=0;i<CompN;i++)
  {
    if(parent.getComponent(i) instanceof MapElem)
    {
    tmp=(MapElem)parent.getComponent(i);
    tmp2=parent.getComponent(i);
    //polodnik
    int P=tmp.getP();
    //rownoleznik
    int R=tmp.getR();
    //szerokosc
    int S=tmp2.getWidth();
    //wysokosc
    int W=tmp2.getHeight();
    double X=P;
    X=((X/180)*polz)+polz;
    double Y=R;
    Y=(Y/90)*rown+rown;
    if(tmp2.getClass()==JMapPoint.class)
    {
      if((int)X+S>parent.getWidth()) X=parent.getWidth()-S/2;
      if((int)X<0) X=0;
      if((int)Y+W>parent.getHeight()) Y=parent.getHeight()-W/2;
      if((int)Y<0) Y=0;
    }
    else if (tmp2.getClass()==JHint.class)
    {
      if((int)X+S>parent.getWidth()) X=parent.getWidth()-(S+5);
      if((int)X<0) X=5;
      if((int)Y+W>parent.getHeight()) Y=parent.getHeight()-(W+5);
      if((int)Y<0) Y=5;
    }
    tmp2.setBounds((int)X,(int)Y, S, W);
    tmp2.repaint();
    }
  }
}

Na pierwszy rzut oka całość może wydawać się skomplikowana. W rzeczywistości jest to bardzo proste. Na początek ustalamy wartości początkowe. Ilość komponentów wewnątrz kontenera, pozycję równika i pozycję południka 0; Potem już pozostaje dla wszystkich komponentów wykonać prosty algorytm:
  1. jeśli komponent jest elementem mapy
  2. pobierz południk, równoleżnik, wysokość i szerokość komponentu
  3. wylicz współrzędną X: X=((południk/180)*południk_zer)+południk_zero;
  4. wylicz współrzędną Y: Y=(równoleżnik/90)*równik+równik;
  5. wprowadź poprawkę jeśli na brzegu mapy,
  6. ustaw wyliczone współrzędne dla komponentu
  7. odrysuj komponent
Mam nadzieję, że powyższy algorytm jest bardziej zrozumiały niż kod powyżej. Pozostaje już tylko opisać kiedy napisana przez nas metoda zostaje wywołana. Metoda ta zostaje wykonana zawsze wtedy, gdy komponent zarządzany przez MapLayout jest odrysowywany, czyli gdy zostaje:
  • pokazany,
  • zmienia rozmiar,
  • zmienia położenie
Dzięki zastosowaniu komponentów graficznych, a nie metody paint jednego komponentu i odpowiednio nepisanego LayoutManager'a mamy pewność, że wszystkie punkty zostaną właściwie rozmieszczone na mapie. Tak jak już mówiłem na wstępie teraz można zacząć korzystać z ich zalet poprze przechwytywanie zdarzeń bezpośrednio przez punkty.

Nie jest to w prawdzie mój pierwszy tutorial, ale w ich pisaniu ciągle jeszcze wprawy nie mam. Będę bardzo wdzięczny za wszelkie komentarze pomocne mi w rozwinieciu się, a przede wszystkim rozwinięciu tego zakątka.

Brak komentarzy:

Prześlij komentarz