Pokazywanie postów oznaczonych etykietą swing. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą swing. Pokaż wszystkie posty

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.

środa, 18 marca 2009

Swing: Własny komponent, EasyWay

Swing to stosunkowo prosty w użyciu zbiór klas do tworzenia GUI. Ma wiele zalet i tyle samo przeciwników co zwolenników. Rozwiązanie to ma już swoje lata, jednak rozwija się dość sprawnie dalej i ciągle pozwala na bardzo sprawne kodowanie. Sun tworząc Swing'a pozostawił stosunkowo dużo możliwości rozwinięcia go. Jednym ze sposobów rozszerzenia Swing jest stworzenie własnego komponentu graficznego. Opiszę tutaj jedną z prostszych metod na stworzenie go.

Zbiór komponentów jest dość duży. Pozornie zawsze znajdzie się czego użyć do stworzenia swojej aplikacji. Czasami by uzyskać właściwy efekt w jakimś specjalistycznym zastosowaniu konieczne jest skorzystanie z kombinacji nawet kilkunastu elementów. Istnieją też takie przypadki, gdy użycie własnego rozwiązania jest po prostu konieczne.

Stosunkowo prosta i chyba najlepsza metoda na stworzenie własnego komponentu to rozszerzenie już istniejącego komponentu o zbliżonych właściwościach zgodnie z zasadami programowania obiektowego. Mówiąc prościej wystarczy stworzyć klasę dziedziczącą z klasy komponentu z pakietu javax.swing.*. Osobiście proponuję użycie klasy JPanel. Jest to bardzo prosty komponent, a co najważniejsze jest kontenerem, więc służy do "przechowania" innych komponentów. Z zasad obiektowości wiemy, że klasa dziedzicząca z JPanel będzie posiadała wszystkie jego właściwości.

Co można zrobić z JPanel? W zasadzie wszystko co potrzeba. Najpierw trzeba wiedzieć jak dokładnie taką klasę przygotować:

public class JCustomComponent extends JPanel
{
  public JCustomComponent()
  {
    super();
  }
}

No i cała filozofia. Co dokładnie robi powyższy kod? Przykład tworzy nową klasę JCustomComponent, która jak na razie zachowuje się dokładnie jak JPanel i w ten sposób można ją wykorzystać. Teraz czas najwyższy na rozszerzenie. Dajmy na to niech ten komponent w tle ma czerwony owal tak na cały swój wymiar.

Nazwy komponentów w Swing są praktycznie takie same jak w AWT. Różnice są niewielkie w ilości dostępnych elementów. Przyjęto więc konwencję, iż nazwy klas Swing zaczynać się będą od dużej litery J. Wskazane jest, aby konwencję tą zachować tworząc nowe klasy, aby jednoznacznie wskazać, pochodzenie komponentu.

Aby wpłynąć na sposób rysowania komponentu wystarczy przeciążyć jedną metodę.

protected void paintComponent(Graphics g)

Za każdym razem gdy komponent jest odrysowywany wywoływana jest właśnie ta metoda. W przypadku większości komponentów tak na prawdę nie zawiera ona nic konkretnego. Rysowaniem jak i obsługą zajmuje się DelegatUI. Nie wpływamy jednak na delegata, tylko na klasę podstawową komponentu. Nie musimy nawet przy tej metodzie wiedzieć, że Delegat istnieje.

Przykład z czerwonym owalem:
public class JCustomComponent extends JPanel
{
  public JCustoComponent()
  {
    super()
  }

  protected void paintComponent(Graphics g)
  {
    g.setColor(Color.red);
    g.drawfilloval(0,0,this.getWidth(), thid.getHeight);
    super.paintComponent(g);
  }
}

Ostatnia linijka w metodzie paintComponent() jest niemal obowiązkowa. Jej zadanie to wywołanie metody rysowania rodzica, czyli klasy JPanel. Jest to szczególnie ważne gdy użyjemy komponentu jako kontenera. Zapewnia to poprawne odrysowanie "dzieci" komponentu dzięki już stworzonym mechanizmom klasy JPanel. Reszta kodu różniącego się od pierwszego przykładu, to tylko rysowanie figury geometrycznej po ustawieniu odpowiedniego koloru.

Proponuję, aby zawsze w konstruktorze komponentu, czyli jeszcze przed jego rysowaniem zadbać o odpowiedni jego wygląd. Mam na myśli ustawienie jego tła na przezroczyste korzystając z metody setBackground(), którą to odziedziczył oczywiście po JPanel. Aby tło było przezroczyste wystarczy stworzyć nowy kolor (klasa Color) z parametrem Alpha ustawionym na 100% (wartość: 255 lub 0xFF)

Co można więcej? W zasadzie co tylko dusza zapragnie. Po pierwsze komponent może dziedziczyć po dowolnym istniejącym komponencie rozszerzając jego możliwości. Przykładowo możemy stworzyć klasę JPasswordField z dowolnym EchoChar'em dziedzicząc z JTextField. Najważniejsze, to aby nie powielać już istniejących rozwiązań. Jeśli potrzebujemy po prostu panelu z tłem w dowolnym kolorze, to lepiej użyć zwykłego JPanel niż tworzyć nową klasę dziedziczącą. W końcu przecież JPanel posiada metodę setBackground(Color).

Jest to raczej krótki wstęp do tworzenia komponentów Swing. Zawiera tylko opis jednej z metod i co ważne prostej, lecz skutecznej. Jej użycie wymaga niewielkiej znajomości Javy. Następnym razem postaram się przybliżyć tworzenie LayoutManager'a dla Swing na przykładzie MapLayoutManager.

Jak można zauważyć każdy post zaopatrzony jest w ilustrację. Te wyposażone w emoticony z tabliczkami można uznać w pewnym sensie za wyjątkowe. Emotsy będą nam nieco przeszkadzać w tekście. Na razie nie ma ich wiele, dlatego te same powtarzają się w obrazkach, ale z czasem przybędzie ich i mam nadzieję czasem kogoś rozbawią. Od razu ostrzegam, że nie jest to komiks internetowy w paskach. Emotsy z założenia mają nie wychodzić poza ramy jednego obrazka, chyba że właśnie gdzieś sobie pójdą ;).