Java FX

kw. 19, 2018 Programowanie

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

JavaFX obrazuje może nieco mniej popularne dziś wykorzystanie języka Java. Jest biblioteką służącą do tworzenia aplikacji okienkowych. Ma ona zastąpić biblioteką Swing, której pierwszą wersję opracowano jeszcze w drugiej połowie lat 90. Swing był krytykowany choćby za to, iż środowiska developerskie (IDE) generowały sporą część kodu, niezbędną do działania aplikacji. O ile dobrze pamiętam, np. w Netbeans jego edycja była wręcz zablokowana.

JavaFX jest niewątpliwie nowszą technologią, bazującą na doświadczeniach technologii front-endowych. Pliki widoku są zbudowane w formie przypominającej HTML. Możliwe jest też używanie CSS w celu poprawienia efektów wizualnych, choć nie jest to konieczne.

Postanowiłem wypróbować tę technologię i zobaczyć, „z czym to się je”.

Prezentacja najciekawszych elementów technologii

Rozpocznę od tego, w jaki sposób przygotowujemy warstwę widoku (okienka) i jak kontrolki są powiązane z kodem źródłowym aplikacji, tj. jak oprogramujemy np. reakcję na kliknięcie przycisku. Najprostszym sposobem zobrazowania, co ma się znajdować w oknie naszej aplikacji, jest wykorzystanie plików FXML.
Przykładowe okienko w aplikacji JAVA FX (logowanie do systemu) może wyglądać tak:

A oto, jak będzie wyglądał plik FXML:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.GridPane?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="main.java.controllers.LoginController">
   <ScrollPane AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0">
      <GridPane alignment="CENTER" hgap="5" vgap="5">
         <padding>
            <Insets top="20" right="20" bottom="20" left="20" />
         </padding>
         <children>
            <Label text="Nazwa użytkownika" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.halignment="RIGHT" />
            <TextField fx:id="usernameField" GridPane.columnIndex="1" GridPane.rowIndex="0" />
            <Label text="Hasło" GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.halignment="RIGHT" />
            <PasswordField fx:id="passwordField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <Button text="Zaloguj" onAction="#loginBtnClick" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.rowSpan="15" />
         </children>
      </GridPane>
   </ScrollPane>
</AnchorPane>

Każdy, kto poznał choćby podstawy tworzenia stron WWW od kuchni, rozpozna podobieństwo do języka HTML. Ściślej rzecz biorąc, FXML został napisany w języku XML, języku pozwalającym zapisać dowolne informacje (więcej o XML: http://webmaster.helion.pl/starocie/xml/xml.htm), na co wskazuje pierwsza liniijka: <?xml version=”1.0″ encoding=”UTF-8″?>.
Dalej mamy listę klas używanych w tym okienku, głównie reprezentacje kontrolek. Tu akurat można doszukać się podobieństwa do samej Javy – jedyną różnicą są znaczniki <? i ?> otaczające każdy import.
Trzecią i najważniejszą częścią FXMLa jest struktura okna. W pierwszym elemencie musimy wskazać przestrzenie nazw, aby kompilator potrafił rozpoznać co kryje się pod nazwami znaczników i atrybutów. Za to właśnie odpowiadają dyrektywy xmlns i xmlns:fx. Nie możemy także pominąć atrybutu fx:controller. Wskazuje on na klasę Javy, która jest kontrolerem naszego okna. JavaFX bazuje bowiem na wzorcu projektowym Model – View – Controller. W tej właśnie klasie znajdą się metody odpowiadające za reakcję na zdarzenia wykonywane w oknie (kilknięcie myszą w odpowiedni przycisk, zmiana tekstu w formatce itp.). Pozostałe atrybuty (maxHeight, maxWidth, minHeight i minWidth) odnoszą się już do rozmiaru okna i w moim przypadku zostały wygenerowane automatycznie.

Myślę, że w tym miejscu warto wspomnieć o rodzajach kontrolek w JavaFX. Każdy przecież może je dobierać we własnym zakresie i nie musi się sugerować moim wyborem.

a) Kontenery, czyli elementy grupujące.
Pane – najprostszy spośród kontenerów. Aby umieścić w nim inne elementy, musimy „na sztywno” zdefiniować miejsce, w którym się pojawią, definiując współrzędne X i Y. Raczej nie do zastosowania w sytuacji, kiedy nie wiemy, jaką rozdzielczość ekranu ma użytkownik, nie mówiąc już o przypadku, gdy zawartość okienka zmienia się w trakcie działania aplikacji (żeby coś przesunąć, musielibyśmy przeliczać współrzędne).
VBox i HBox – kontenery te układają kolejno poszczególne elementy, przy czym VBox robi to wierszami (jeden pod drugim), zaś HBox kolumnami (obok siebie). Można zdefiniować odstęp w pikselach jaki ma być między kolejnymi elementami (atrybut spacing). Domyślnie nie ma odstępu.
Najbardziej użyteczne są w sytuacji, kiedy mamy listę jakichś elementów i dla każdego z nich chcemy zdefiniować określone kontrolki, na przykład lista użytkowników kursu, z których dla każdego chcemy mieć etykietkę z imieniem i nazwiskiem oraz przyciski „Usuń”, czy „Zmień grupę”. Całą listę zgrupowałem w VBox-ie, a dla każdego elementu robiłem HBox, który umieszczał w jednym wierszu kontrolkę Label i dwie kontrolki Button.
StackPane – kontener układa elementy jeden nad drugim (stos). Jedynym zastosowaniem, jakie przyszło mi do głowy, jest rysowanie elementów jak na przykładzie tutaj: https://www.tutorialspoint.com/javafx/layout_stackpane.htm . Osobiście nie używałem.
BorderPane – układanie elementów w określonych częściach okna, dostępne pozycje to: Top, Left, Right, Bottom i Center. Niewątpliwie przydatne, choć będziemy potrzebowali je zagnieżdżać z innymi kontenerami albo z innymi instancjami BorderPane.
GridPane – ustawia elementy tak jakby w tabeli. Każdy element będący wewnątrz GridPane powinien mieć zdefiniowany wiersz i kolumnę w jakiej ma zostać zlokalizowany. Można więc go użyć zamiennie z BorderPane.
AnchorPane – elementy wewnątrz niego są bezpośrednio „przyklejone” do elementu rodzica, z zachowaniem z góry zdefinowanych odstępów.
Dostępne są również inne kontenery takie jak: TilePane czy FlowPane.

b) Inne elementy.
Generalnie JavaFX udostępnia zestaw standardowych kontrolek jak np. etykieta tekstowa (Label), pole tekstowe (TextField), elementy wyboru jednokrotnego (RadioButton) lub wielokrotnego (CheckBox) lista rozwijana (ChoiceBox), przycisk (Button), czy też pasek menu (Menu i MenuItem). Są też mniej oczywiste, jak np. edytor tekstowy (HTMLEditor) czy łącze (Hyperlink). Możemy też rysować różne figury geometryczne.

Jeśli samodzielne edytowanie pliku FXML jest dla programisty zbyt skomplikowane lub czasochłonne, może nam pomóc program Scene Builder (http://gluonhq.com/products/scene-builder/). Umożliwia on tworzenie GUI w formie wizualnej (WYSIWYG: What You See Is What You Get) i sam przetwarza wyniki naszej pracy na tekstowy plik FXML. Istnieje możliwość jego integracji z IDE przynajmniej w wypadku IntelliJ (innych nie próbowałem).

W FXML możemy też definiować widoki częściowe, w postaci kontenera zawierającego kontrolki, które mają się pojawić na ekranie w wyniku działania jakiejś operacji, w bieżącym oknie.

Warto też dodać, że wszystkie kontenery i kontrolki możemy oczywiście definiować bezpośrednio w kodzie Javy, w klasie kontrolera.

Zdarzenia i ich realizacja.

Jeśli chcemy je zdefiniować w FXML, powinniśmy przy kontrolce, której zdarzenie ma dotyczyć, dopisać atrybut onAction, onMouseClicked, itp. zależnie od zdarzenia, którego obsługę chcielibyśmy zdefiniować. Jako wartość atrybutu wpisujemy nazwę metody w kontrolerze, poprzedzoną znakiem #. Uwaga, metoda powinna przyjmować jako jedyny argument obiekt typu ActionEvent lub jego obiekt pochodny w zależności od rodzaju zdarzenia. Powinniśmy także oznaczyć metodę annotacją @FXML, jak również przypisać identyfikator do elementu w FXML oraz umieścić instancję kontrolki jako pole w klasie kontrolera o tej samej nazwie co identyfikator w FXML, jak również opisać te pole także annotacją @FXML.

Z kolei w przypadku przypisywania akcji w kodzie Javy, z dużą pomocą przychodzi nam Java 8. Gdyby nie wprowadzona w niej możliwość przypisywania metody za pomocą podwójnego dwukropka: control.setOnAction(this::methodName), to dynamiczne wiązanie reakcji na zdarzenia byłoby mocno utrudnione. Możemy również skorzystać z interfejsu lambda, np: control.setOnAction(e -> doSomething(parameter1, parameter2)).

Mankamenty

Jest utrudnione bindowanie, czyli wiązanie wartości kontrolki ze polem w kontrolerze w niektórych kontrolkach (co czynimy zwykle aby łatwiej odczytywać zaznaczony element). Dotyczy to na przykład listy rozwijanej.
Źle rozwiązano sytuację, w której – iterując po wartościach jakiejś listy – wyświetlam listę elementów tej listy i chcę mieć przycisk, który coś robi. Chciałbym żeby wywoływał metodę z parametrem wskazującym na identyfikator tegoż elementu. Nic z tego, bo wyskoczy: „variable used in lambda expression should be final or effectively final”. W AngularJS bez problemu robiłem coś podobnego. Tutaj musiałem się uciec do stworzenia kontrolki rozszerzającej kontrolkę „Button” i zawierającej pole typu String na identyfikator. Do tego jeszcze w metodzie obsługującej zdarzenie musiałem robić mało estetyczne rzutowanie.

Poniżej krótkie wprowadzenie bardziej techniczne.

Każdy, kto już pisał program w języku Java wie, że w głównej klasie naszej aplikacji powinna znaleźć się metoda:

public static void main(String[] args) {
    // ...
}

Tę metodę uruchamia maszyna wirtualna środowiska i wykonuje zawarte w niej instrukcje.

JavaFX zastosowało klasę abstrakcyjną Application z pakietu javafx.application, które odpowiada za rozruch naszego programu.
Zadaniem programisty jest:
a) Poinformowanie kompilatora, że nasza klasa rozszerza klasę Application.
b) Umieszczenie w metodzie main() instrukcji launch(args). Metoda „launch” została zaimplementowana w klasie Application i nie musimy się nią zajmować.
c) Umieszczenie oraz implementacja w naszej klasie metody „start”:

public void start(Stage primaryStage) throws Exception {
    // ...
}

To właśnie od tego miejsca będziemy definiować wyświetlanie naszych okien i wszystkiego, co będzie się działo w naszym programie.

Dla niezorientowanych, poniżej szkielet bazowej klasy:

package {nazwa naszego pakietu};

import javafx.application.Application;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    public void start(Stage primaryStage) throws Exception {
        // implementacja aplikacji
    }

}

Klasa Stage, której instancja jest argumentem wejściowym funkcji start() to, w uproszczeniu, reprezentacja okna naszej aplikacji.

Po bardziej szczegółowy tutorial techniczny proponuję przejść np. na stronę https://www.tutorialspoint.com/javafx/ (w jęz. ang.)

Możesz skomentować ten wpis w serwisach społecznościowych: Linkedin (przejdź) lub Facebook (przejdź).

Przez Michał Ch.