poniedziałek, 2 grudnia 2013

Bardzo bogate klienty, JavaFX - tutorial #01


Do tej pory programując w Javie, wszelkie programy z graficznym interfejsem tworzyłem z wykorzystaniem biblioteki Swing, aż tu nagle, całkiem niedawno usłyszałem o czymś takim jak JavaFX.


Słowo wstępu:



Czym jest JavaFX ?


Jest to technologia przeznaczona do tworzenia bogatych klientów aplikacji, czyli po prostu ma ułatwiać kreowanie pięknego, stylowego i nowoczesnego GUI (Graphical User Interface). Właściwie JavaFX można traktować trochę jako bibliotekę będącą następcą wysłużonego już Swinga.
Przyjrzyjmy się zatem co konkretnie oferuje ta technologia:

  • Tworzenie GUI przy pomocy arkuszy CSS. Myślę, że ten kto kiedykolwiek próbował swoich sił w pisaniu stron www, wie że jest to doskonałe rozwiązanie rozdzielenia strony graficznej od funkcjonalnej. Dzięki temu możemy dowolnie zmieniać wygląd aplikacji, nie wprowadzając żadnych zmian w jej kodzie. Pozwala to zachować jego czystość oraz łatwo tworzyć wiele szablonów graficznych, wczytywanych według potrzeb.
  • Biblioteki dla bogatych interfejsów. JavaFX udostępnia odświeżone, znane wcześniej ze Swinga jak i nowe komponenty. Pozwala łatwo implementować ciekawe efekty graficzne oraz szybko tworzyć grafikę 2D oraz 3D.
  • Integracja z innymi technologiami. JavaFX jest kompatybilna z najnowszymi JDK (od wersji 7). Jej kod można swobodnie łączyć z natywnymi bibliotekami Java, Swingiem, czy aplikacjami webowymi - JavaScript, HTML. Tak jak inne aplikacje Java, zapewnia działanie na każdej platformie z zainstalowaną JVM (Java Virtual Machine).
  • Sprzętowe przetwarzanie grafiki. Dzięki wykorzystaniu potokowego przetwarzania grafiki z wykorzystaniem kart graficznych, można tworzyć efektowne i płynne efekty wizualne.

Jak widać możliwości wyżej wymienionej biblioteki są ciekawe i powinny bardzo ułatwić tworzenie estetycznych i dynamicznych interfejsów użytkownika. Zachęcony tą obietnicą wziąłem się za naukę JavaFX i postanowiłem, że to co uda mi się opanować, będę wrzucał tutaj w formie tutoriali. Tak więc zapraszam do lektury i nauki :)


Dla kogo ten tutorial ?


Nie będę ukrywał, że aby sprawnie przejść przez ten kurs dobrze by było pierwsze kroki w programowaniu, oraz doświadczenie w jakimś obiektowym języku (najlepiej Javie) mieć już za sobą. Jednak nie od dziś wiadomo, że dla chcącego nic trudnego, więc nie będę nikogo zniechęcać. Znajomość biblioteki Swing nie jest tutaj konieczna, choć daje lepsze rozeznanie podczas pisania kodu, ponieważ wiele rzeczy jest  rozwiązanych bardzo podobnie.
Jeżeli ktoś preferuje wersję wideo tutoriala to zapraszam tutaj


Jak zacząć ?


Jeżeli korzystasz z Eclipse'a, chyba najlepszym rozwiązaniem będzie ściągnięcie plugina e(fx)clipse. Instrukcję krok po kroku dot. instalacji znajdziecie tutaj. Jeżeli Twoim IDE jest NetBeans to zdaje się, że od wersji 7.2 JavaFX jest po prostu wbudowana, podobnie jest z IntelliJ IDEA. Jeżeli żaden z powyższych - najlepiej rozgryź jak nakłonić Twoje IDE do współpracy z JavaFX i napisz jak to zrobić w komentarzu, aby inni mieli łatwiej :).



Pierwszy program.



Szkielet


Kiedy środowisko mamy już przygotowane (ja będę używał Eclipse'a), możemy przejść do właściwego kursu JavaFX. Ponieważ klasyczne 'Hello World' jest trochę ubogie, zdecydowałem zacząć od prostego formularza i tak dzisiaj dowiecie się:
- Jak działają aplikacje JavaFX i czym różnią się od innych programów Java.
- Jak stworzyć prosty formularz logujący.
- Jak przechwycić i obsłużyć zdarzenie kliknięcia w przycisk.
- Jak za pomocą arkusza CSS dostosować wygląd aplikacji.

Zaczynamy! Na początku tworzymy nowy projekt JavaFX. W Eclipse: File -> New -> Project... -> JavaFX -> JavaFX Project. Powinien stworzyć się nowy projekt z domyślną klasa Main.java i pustym arkuszem CSS. Jeżeli tak się nie stało to należy je utworzyć samemu. Wówczas, całkiem prawdopodobne, że plik z rozszerzeniem .css trzeba wrzucić do folderu src (tam gdzie główna klasa) za pomocą funkcji import. (Eclipse) File -> Import... -> FileSystem -> Browse. Inaczej może nie być widoczny. Jeżeli powyższe pliki powstały automatycznie to proponuję zmienić nazwę klasy z Main.java na coś więcej mówiącą nazwę - to dobra praktyka zarówno dla nazw plików jak i zmiennych i metod. U mnie będzie to LoginForm.java.

Jeżeli wszystko już gotowe to zabieramy się za pisanie kodu. Pierwsze co powinno się znaleźć w naszym pliku to taki prosty szkielet:


package application;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class LoginForm extends Application
{
 private Scene scene;
 private GridPane grid;
 
 public void start(Stage primaryStage)
 {
  primaryStage.setTitle("JavaFX Login Form");
  
  grid = new GridPane();
  grid.setAlignment(Pos.CENTER);
  grid.setHgap(10);
  grid.setVgap(10);
  grid.setPadding(new Insets(25, 25, 25, 25));
  
  scene = new Scene(grid, 300, 275);
  primaryStage.setScene(scene);
  
  primaryStage.show();
 }

 public static void main(String[] args)
 {
  launch(args);
 }
}
Warto zauważyć w powyższym listingu, że główna klasa naszego programu rozszerza klasę Application, i implementuje metodę start(), która jest głównym punktem wyjściowym każdej aplikacji JavaFX.

Następnie mamy deklarację dwóch zmiennych, które będą nam potrzebne:

 private Scene scene;
 private GridPane grid;
Zmienna scene klasy Scene będzie głównym kontenerem dla wszystkich komponentów, natomiast zmienną grid tworzymy jako podstawę dla formularza. Klasa GridPane odpowiada bowiem za kompozycję elementów w niej umieszczonych rozkładając je na wirtualnej siatce. Jej działanie jest analogiczne do znanego ze Swinga menadżera rozkładu - GridLayout.

Kolejny, kluczowy dla nas element to metoda start() i to co się wewnątrz niej dzieje.

 public void start(Stage primaryStage)
 {
  primaryStage.setTitle("JavaFX Login Form");
  
  grid = new GridPane();
  grid.setAlignment(Pos.CENTER);
  grid.setHgap(10);
  grid.setVgap(10);
  grid.setPadding(new Insets(25, 25, 25, 25));
  
  scene = new Scene(grid, 300, 275);
  primaryStage.setScene(scene);
  
  primaryStage.show();
 }
Po pierwsze metoda ta przyjmuje jakiś dziwny parametr klasy primaryStage. Coż to takiego? Jest to nadrzędny, główny kontener dla całej aplikacji - taka ramka trzymająca wszystkie komponenty (JFrame w Swing). Dalej mamy ustawienie tytułu, inicjalizację obiektu grid i parę ustawień do niego:

grid.setAlignment(Pos.CENTER); - ustawia wyrównanie komponentów do środka.
grid.setHgap(10); - ustawia odstępy między kolumnami siatki / tabeli.
grid.setVgap(10); - ustawia odstępy między wierszami.
grid.setPadding(new Insets(25, 25, 25, 25)); - ustawia wewnętrzne marginesy

Dalej inicjalizujemy scenę, o której trochę powiedzieliśmy już wcześniej, podając jako argumenty panel grid, szerokość i wysokość. Ustawiamy scenę dla najwyższego kontenera i wywołujemy funkcję show(), która sprawi, że cała aplikacja stanie się widoczna.
Warto jeszcze dodać, że metoda main() znajdująca się pod koniec naszego szkieletu nie jest konieczna w aplikacjach JavaFX. Podczas tworzenia pliku .jar narzędzie JavaFX Packager osadza w nim JavaFX Launchera, który korzysta z metody start(). Niemniej jednak, przydaje się umieścić tę metodę aby uruchomić plik .jar stworzony bez JavaFX Launchera, na przykład podczas użycia niezbyt dobrze zintegrowanego IDE. Poza tym aplikacje Swing, które zawierają kod JavaFX wymagają już metody main().

Teraz po kompilacji i uruchomieniu kodu powinniśmy otrzymać okienko podobne do tego:


Może jeszcze nie wygląda jak typowy formularz logowania, ale przynajmniej już coś jest :) Kolejnym krokiem będzie dodanie zawartości w formie różnych komponentów i kontrolek.


Dodanie zawartości okna


Do wcześniej zadeklarowanych zmiennych grid i scene dodajmy jeszcze kilka:

 private Text sceneTitle;
 private Label userNameLabel, passwordLabel;
 private TextField userNameField;
 private PasswordField passwordField;
 private Button button;
 private HBox hBoxPane;
 private final Text notification = new Text();
 private String cssPath;

Oczywiście należy pamiętać o dołączeniu odpowiednich importów. W przypadku Eclipse'a wygodnie jest posłużyć się skrótem ctrl+shift+o, który powoduje ich automatyczną organizację.
Jeżeli chodzi o powyższe zmienne (i stałe) to omówimy je za chwilę, przy okazji ich wdrażania do sceny. Aby to zrobić, przed linijką inicjalizującą scenę dopiszmy następujące:

 sceneTitle = new Text("Welcome");
 grid.add(sceneTitle, 0, 0, 2, 1);
  
 userNameLabel = new Label("User Name:");
 grid.add(userNameLabel, 0, 1);
  
 userNameField = new TextField();
 grid.add(userNameField, 1, 1);
  
 passwordLabel = new Label("Password:");
 grid.add(passwordLabel, 0, 2);
  
 passwordField = new PasswordField();
 grid.add(passwordField, 1, 2);
  
 button = new Button("Sign in");
 hBoxPane = new HBox(10);
 hBoxPane.setAlignment(Pos.BOTTOM_RIGHT);
 hBoxPane.getChildren().add(button);
 grid.add(hBoxPane, 1, 4);

Myślę, że poza pojedynczymi wierszami, kod ten powinien być jasny. Zmienną sceneTitle inicjalizujemy od razu nadając jej wartość "Welcome", następnie dodajemy obiekt do siatki podając jako argumenty kolejno: referencję obiektu Text, indeks kolumny siatki, indeks wiersza, liczba kolumn do podziału potomnych komórek, liczba wierszy do podziału potomnych komórek. Dwa ostatnie argumenty mogą wydawać się niezrozumiałe, ale nie wiem jak to polsku, zwięźle opisać, więc proponuję po prostu pobawić się tymi wartościami i sprawdzić co się zmieni. Następne kontrolki dodajemy do siatki podając już tylko indeksy kolumn i wierszy. Ostatnią kontrolką jest przycisk button i jak widać nie instalujemy go bezpośrednio w siatce, ale w obiekcie klasy HBox, który dopiero dodajemy do obiektu grid. Co nam to daje? Otóż klasa HBox stanowi  panel rozkładu, któremu podajemy atrybut przylegania. Dzięki temu zabiegowi, przycisk będzie zawsze znajdował się w prawym dolnym rogu komórki w siatce grid.

Jeżeli chcemy mieć lepsze rozeznanie, możemy kazać wyświetlić linie siatki dopisując jeszcze jedno ustawienie grid'a :
grid.setGridLinesVisible(true);

Po uruchomieniu aplikacji (bez pokazywania linii siatki), naszym oczom powinno ukazać się mniej więcej takie okienko:

Jak widać już trochę przypomina to formularz, ale wygląda on nieco biednie, więc teraz zajmiemy się tym co najciekawsze, czyli dostosujemy wygląd formularza za pomocą arkusza css i dodatkowo obsłużymy zdarzenie kliknięcia w przycisk 'Sign in'.

Może zacznijmy od tego drugiego, a zabawę z wyglądem zostawimy sobie na deser.


Obsługa zdarzenia wciśnięcia przycisku


Aby przechwycić i obsłużyć zdarzenie kliknięcia w przycisk musimy zaimplementować interfejs EventHandler.
Możemy to zrobić za pomocą osobnej klasy będącej słuchaczem wszystkich zdarzeń - to dobra praktyka w przypadku większych projektów, która pozwala zachować porządek w kodzie. Nasz programik jednak nie będzie zbyt rozbudowany więc posłużymy się anonimową klasą wewnętrzną, która sprawi, że efektem kliknięcia w button będzie wyświetlenie komunikatu informującego nas, że cała procedura zadziałała. Poniższy kod dopisujemy więc przed inicjalizacją sceny.
 grid.add(notification, 1, 6);
  
 button.setOnAction(new EventHandler<ActionEvent>() {
   
  public void handle(ActionEvent event)
  {
   notification.setText("'Sign in' button pressed");
  }
 });

Teraz nasza aplikacja, po wciśnięciu przyciku 'Sign in' powinna wyglądać tak:



Dostosowanie layoutu za pomocą arkusza CSS


Całą funkcjonalność oraz projekt rozmieszczenia elementów GUI mamy gotowe, więc pozostało już tylko podrasować stronę wizualną programu.
Arkusze css odwołują się do obiektów aplikacji za pomocą nazw klas oraz konkretnych id. i o ile aby odwołać się do całej klasy obiektów, nie musimy nic więcej dodawać w pliku .java to by odwołać się do id, najpierw musimy je przypisać. Od tego zaczniemy. Ja będę chciał dodać jakiś obraz w tle, zmienić wygląd etykiet oraz buttona i do tych elementów odwołam się za pomocą nazw klas. Chciałbym również podrasować tytuł sceny i wyskakujący komunikat,  obiekty za nie odpowiedzialne pochodzą z tej samej klasy Text, ale ich wygląd powinien znacznie się różnić, więc tym elementom należy dodać id.
Robimy to dopisując po linijce inicjalizującej sceneTitle nastepującą:
sceneTitle.setId("sceneTitle");
oraz jeszcze jedną, przed wierszem dodającym komunikat do siatki:
notification.setId("notification");

Oczywiście nie trzeba nadawać takich samych id jak nazwa referencji, ale wydaje mi się, że poprawia to orientację w kodzie.
Ponadto musimy jeszcze naszej scenie podać ścieżkę do arkusza css, z którego będziemy korzystać. W tym celu, przed metodą show(), dodajemy dwie linijki:

 cssPath = this.getClass().getResource("application.css").toExternalForm();
 scene.getStylesheets().add(cssPath);
Oczywiście u Was nazwa arkusza może się różnić.

Pozostało już tylko otworzyć plik css i nadać odpowiednie właściwości elementom GUI. Najpierw jednak pokrótce  omówię składnie arkuszy css.
Zapis typu .nazwa_klasy oznacza odwołanie się do całej klasy obiektów, natomiast #identyfikator odwołuje się do konkretnych obiektów o podanym id. Elementy podane za pomocą klasy lub id nazywane są selektorami, natomiast poprzedzone myślnikiem właściwościami. Tak więc np. .button, lub #notification to selektory, natomiast -fx-text-fill: #103550; to właściwość z parametrem, będącym oznaczeniem koloru.
Właściwości obiektów umieszczamy w bloku selektora (lub paru selektorów) ograniczonego nawiasami klamrowymi.

Zaczniemy od dodania tła dla sceny. W tym celu w arkuszu wpisujemy poniższy kod:

.root
{
-fx-background-imageurl("bg_hex2.jpg");
}
Selektor .root oznacza odwołanie do nadrzędnego węzła sceny. Dalej pomiędzy blokiem nawiasów klamrowych dodajemy właściwość odpowiedzialną za ustawienie obrazka w tle: -fx-background-imageurl("path");. Argument "path" to ścieżka dostępu do pliku z grafiką. Możecie użyć własnej lub mojej, pobierając poniższy obrazek:


Aby wszystko zadziałało grafikę należy zaimportować do folderu z kodem źródłowym lub ew. zmienić odpowiednio ścieżkę dostępu.

Kolejnym elementem do podrasowania są etykiety. Poniżej poprzedniego bloku dodajemy kolejny:

.label
{
-fx-font-size: 12px;
-fx-font-weight: bold;
-fx-text-fill: #B0B0B0;
}
Jak nietrudno się domyślić pierwsza z podanych właściwości odpowiada za rozmiar czcionki, a jej wartość podana jest w pikselach. Druga ustawia grubość fontów, a trzecia ich kolor poprzedzony znakiem # i podany w systemie heksadecymalnym - czyli szesnastkowym - gdzie pierwsze dwie cyfry odpowiadają za kanał czerwony, kolejne dwie za zielony i ostatnia para za kanał niebieski (system RGB).

W następnym kroku dostosujemy wygląd tytułu sceny oraz powiadomienia:

#sceneTitle
{
-fx-font-size: 32px;
-fx-font-family: "Arial Black";
-fx-fill: #EAEAEA;
-fx-effect: innershadow(three-pass-box, rgba(0,0,0,0.7), 6, 0.0, 0, 2);
}

#notification
{
-fx-fill: #FF7070;
-fx-font-weight: bold;
-fx-effectdropshadow(gaussian, rgba(255, 50, 50, 0.5), 0, 0, 0, 1);
}
Myślę, że wszystkie właściwości poza -fx-effect, nie wymagają komentarza. Te natomiast dodają efekty cienowania gdzie pierwszy parametr oznacza rodzaj, kolejny ustawia kolor i poziom przezroczystości (rgb w sys. dziesiętnym + kanał alfa), a ostatnie cztery definiują sposób cieniowania.
Po bardziej precyzyjne omówienie parametrów odsyłam do dokumentacji.

Na koniec podmalujemy trochę nasz przycisk:

.button
{
-fx-background-color:
        linear-gradient(#40ccda, #208ada),
        radial-gradient(center 50% -40%, radius 200%, #40ccda 45%, #208ada 50%);
    -fx-background-radius: 6, 5;
    -fx-background-insets: 0, 1;
    -fx-effectdropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
    -fx-text-fill: #104060;
}

.button:hover
{
-fx-background-color:
        linear-gradient(#50e0ff, #20a0ff),
        radial-gradient(center 50% -40%, radius 200%, #50e0ff 45%, #20a0ff 50%);
    -fx-text-fill: #103550;
}

.button:pressed
{
-fx-background-color:
#40cfff,
        linear-gradient(rgba(100,180,240,0.5), rgba(15,70,100,0.7), rgba(100,180,240,0.5));
    -fx-text-fill: #60e0ff;
}

W powyższym przypadku trzy razy odwołujemy się do klasy .button, tyle że za drugim razem z dodatkową podklasa hover, a trzecim z podklasą pressed podanymi po przecinku. Podklasy te pozwalają nam zdefiniować wygląd elementu, na który użytkownik najedzie kursorem (hover) i na który kliknie (pressed).
Warto zauważyć, że podczas definiowania właściwości podklas, wystarczy używać jedynie tych, które ulegną zmianie względem orginalnej klasy. Reszta zostanie pobrana z oryginału.

Po wszystkich opisanych wyżej zabiegach kosmetycznych, nasza aplikacja powinna wyglądać tak jak na załączonym niżej obrazku, chyba że bawiliście się pokazanymi funkcjami i zmienialiście wygląd wedle własnego uznania - do czego zresztą zachęcam :) . U mnie wygląda to tak:

To tyle jeżeli chodzi o wstęp do JavaFX. Obiecuję, że jak tylko nauczę się nowych, ciekawych rzeczy w tym temacie na pewno pojawią się kolejne części kursu. Cały kod aplikacji oraz grafikę znajdziecie niżej w sekcji "Źródła", natomiast dodatkowe materiały w sekcji "Linki" :) .

Źródła:

Cały kod aplikacji, arkusz css oraz grafika: źródło

Linki:

Videotutorial: link
Strona projektu e(fx)clipse: link
Instrukcja instalacji pluginu e(fx)clipse: link
Strona główna dot. JavaFX: link
Hello World w JavaFX: link
Przykłady stylizacji buttonów w JavaFX za pomocą arkuszy CSS: link
Dokumentacja JavaFX: link
Dokumentacja CSS dla JavaFX: link

1 komentarz: