Якось у мене виникла ідея написати невеликий настільний додаток для своїх потреб – щось на кшталт невеликого словника для вивчення іноземних слів – і я почав ламати голову: а як би мені це зробити? Звичайно, перше, що мені спало на думку, – Swing. Усі напевно чули про Swing. Це бібліотека для створення інтерфейсів користувача. У зв'язку з тим, що наш улюблений Oracle ще не повністю відмовився від Swing, він не вважається застарілим, і програми на ньому, як і раніше, працюють. Однак Swing більше не модернізується, і хлопці з Oracle дали нам зрозуміти, що за JavaFX – майбутнє. Та й, по суті, JavaFX використовує компоненти Swing як постачальника послуг.

Що таке JavaFX?

JavaFX – це інструментарій GUI для Java. Тут буде невеликий відступ, і ми пригадаємо, що таке GUI: Graphical User Interface – графічний інтерфейс користувача – це різновид інтерфейсу користувача, у якому всі елементи (кнопки, меню, піктограми, списки) представлені користувачеві на дисплеї та виконані у вигляді зображень і графічних елементів. На відміну від інтерфейсу командного рядка, GUI має довільний доступ до видимих об'єктів за допомогою пристроїв введення. Найчастіше елементи інтерфейсу реалізовані у вигляді метафор і відображають їхні властивості та призначення для полегшення розуміння користувачем. JavaFX орієнтований на створення ігор і настільних додатків на Java. По суті, він замінить Swing через запропонований новий інструментарій GUI для Java. Також він дозволяє нам стилізувати файли компонування GUI (XML) і робити їх елегантнішими за допомогою CSS, подібно до того, як ми звикли це робити у вебдодатках. JavaFX додатково працює з інтегрованою 3D-графікою, а також аудіо, відео та вбудованими мережевими додатками в межах єдиного інструментарію GUI. Він простий в освоєнні та добре оптимізований. JavaFX підтримує багато операційних систем, а також Windows, UNIX-системи та Mac OS.Введення в Java FX - 1

Особливості JavaFX:

  • JavaFX спочатку поставляється з великим набором частин графічного інтерфейсу, таких як будь-які кнопки, текстові поля, таблиці, дерева, меню, діаграми і т.д., що, своєю чергою, заощадить нам вагон часу.
  • JavaFX часто юзає стилі CSS, і ми зможемо використовувати спеціальний формат FXML для створення GUI, а не робити це в коді Java. Це полегшує швидке розміщення графічного інтерфейсу користувача або зміну зовнішнього вигляду чи композиції без необхідності довго гратися в коді Java.
  • JavaFX має готові для використання частини діаграми, тому нам не потрібно писати їх з нуля будь-коли, коли вам потрібна базова діаграма.
  • JavaFX додатково поставляється з підтримкою 3D-графіки, яка часто корисна, якщо ми розробляємо якусь гру або подібні програми.
Давайте трохи пройдемося по основних складових нашого вікна:
  • Stage – по суті, це навколишнє вікно, яке використовується як початкове полотно і містить інші компоненти. У додатку може бути кілька stage, але один такий компонент має бути в будь-якому випадку. По суті, Stage є основним контейнером і точкою входу.
  • Scene – відображає зміст stage (прямо матрьошка). Кожен stage може містити кілька компонентів – scene, які можна перемикати між собою. Усередині це реалізується графом об'єктів, який називається Scene Graph (де кожен елемент – вузол, ще званий Node).
  • Node – це елементи керування, наприклад, кнопки, мітки або навіть макети (layout), усередині яких може бути кілька вкладених компонентів. Кожна сцена (scene) може мати один вкладений вузол (node), але це може бути макет (layout) з кількома компонентами. Вкладеність може бути багаторівневою, коли макети містять інші макети та звичайні компоненти. Кожен такий вузол має свій ідентифікатор, стиль, ефекти, стан, обробники подій.
Введення в Java FX - 2Отже, давайте рухатися трохи в бік коду. Так як у мене використовується Java 8, мені не потрібно підтягувати ніякі залежності, оскільки JavaFX за замовчуванням є в JDK (як і в Java 9, 10), але якщо у нас Java 11+, то потрібно піти в Maven Repository і стягнути звідти залежності:

JavaFX: приклади використання

Створюємо звичайний клас із методом main (точкою входу):

public class AppFX extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
pimarySatge.show();
    }
}
Тут наш клас успадковується від javafx.application.Application (що у нас з коробки, бугага). У main викликаємо статичний метод Application.launch() для запуску нашого вікна. Також у нас IDEA буде лаятися на те, що ми не реалізували метод Application.start, що ми, зрештою, і робимо. Навіщо він потрібен? А щоб можна було керувати властивостями (функціоналом) нашого вікна. Для цього у нас використовується вхідний аргумент primaryStage, у якого ми викликаємо метод show(), щоб можна було побачити вікно, яке запускається з main. Давайте трохи заповнимо наш метод start:

public void start(Stage primaryStage) throws Exception {
    primaryStage.setTitle("Dogs application");
    primaryStage.setWidth(500);
    primaryStage.setHeight(400);

    InputStream iconStream =
    getClass().getResourceAsStream("/images/someImage.png");
    Image image = new Image(iconStream);
    primaryStage.getIcons().add(image);

    Button button = new Button("WOF WOF ???'");

    button.setOnAction(e -< {
      Alert alert = new Alert(Alert.AlertType.INFORMATION, "WOF WOF WOF!!!");
        alert.showAndWait();
    });
    Scene primaryScene = new Scene(button);
    primaryStage.setScene(primaryScene);

    primaryStage.show();
}
Отже, що ми тут бачимо? Пробіжимося рядково: 2 - задаємо назву самого вікна (stage) 3,4 - задаємо його розміри 6,7 - задаємо шлях потоку, що читає, до файлу (іконці)Введення в Java FX - 38 – створюємо файл як об'єкт Image, який пов'язаний із реальним файлом потоком, що передається в конструкторі. 9 – задаємо іконку у верхню панель вікна. 11 – створюємо об'єкт кнопки. 13–16 – задаємо реакцію при натисканні кнопки. 17 – створюємо сцену, куди поміщаємо нашу кнопку. 18 – сцену поміщаємо на наше загальне вікно. 20 – задаємо прапор видимості для вікна. І як результат – віконце для вітання наших улюблених песелів:Введення в Java FX - 4Все виглядає в рази простіше, ніж Swing, чи не так? Але це ще не кінець. Повністю писати весь код для відображення програми – не добре, потрібно його якось ділити, щоб зробити більш зрозумілим (графічні складові – в одні кошики, логіку – в інші). І тут на сцену виходить хмл… О боже мій, XML? Саме так. А саме – використовується його специфічна реалізація для JavaFX – FXML, у якій ми визначаємо графічні компоненти додатка та їхні властивості (там різні розміри та інше), а потім пов'язуємо з контролером, який і допомагає керувати логікою. Давайте розглянемо приклад такого XML:

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<VBox xmlns="http://javafx.com/javafx"  xmlns:fx="http://javafx.com/fxml"
   id="Dogs application" prefHeight="200" prefWidth="300" alignment="center">

  <Label text="Wow wow?"/>

  <Button fx:id="mainButton" text="Greeting" onAction="buttonClicked()"/>

    <fx:script>
       function buttonClicked() {
            mainButton.setText("Wow wow wow!!!")
       }
    </fx:script>
</VBox>
2 – мова сценаріїв, якою ми користуємося. 4–6 – імпортовані дані. 8–9 – VBox – контейнер, який розміщує підкомпоненти в одному рядку. 11 – виводимо якийсь текст. 13 – кнопка, при натисканні якої ми юзаємо метод, описаний у скрипті на 15–18 рядку. Тут повинен бути код виклику цього XML-файлу в методі start, але зараз це не так важливо, і ми це опустимо (нижче буде приклад підтягування цього файлу). Отже, XML – це, звичайно, добре (та не дуже), але вручну писати їх дуже заморочено, хіба це не минуле століття?Введення в Java FX - 5

Знайомство з JavaFX SceneBuilder

Саме на цьому моменті на сцену виходить (барабанний дріб) – SceneBuilder. JavaFX Scene Builder – це інструмент, за допомогою якого ми можемо конструювати наші вікна у вигляді графічного інтерфейсу, а потім зберігати їх, і ця програма на основі результату буде конструювати XML-файли, які ми будемо підтягувати в нашому додатку. Якось так виглядає інтерфейс цього FXML-будівельника:Введення в Java FX - 6

Невеликий відступ. JavaFX уроки

Деталі встановлення я опущу, і докладне вивчення цього інструменту теж. Це теми, які варто дослідити додатково. Тому все ж таки залишу пару цікавих посилань на JavaFX-уроки: раз (онлайн-підручник з JavaFX) і два (ще один непоганий туторіал). Давайте трохи пробіжимося невеликим прикладом, який я накидав. У результаті в мене вийшло щось на кшталт:Введення в Java FX - 7
(таке собі віконце для обліку собак)
При виборі песеля та натисканні кнопки Delete собака видаляється з нашого списку. При виборі чотирилапого друга і зміні його полів, а після натискання кнопки Edit – інфа собачки оновлюється. Коли натискаємо кнопку New, вилазить віконце для створення запису нового собаки (для початку – його імені):Введення в Java FX - 8Після цього тиснемо Save і заповнюємо в першому вікні інші її поля, а потім тиснемо кнопку Edit для збереження. Звучить нескладно, вірно? Давайте подивимося, як це буде виглядати у нас у програмі Java. Для початку я просто залишу тут XML-макети для цих двох вікон, згенерованих у SceneBuilder: Перше (базове):

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<AnchorPane prefHeight="300.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tutorial.controller.BaseController">
   <children>
      <SplitPane dividerPositions="0.29797979797979796" prefHeight="300.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <items>
          <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
               <children>
                  <TableView fx:id="dogs" layoutX="-2.0" layoutY="-4.0" prefHeight="307.0" prefWidth="190.0" AnchorPane.bottomAnchor="-5.0" AnchorPane.leftAnchor="-2.0" AnchorPane.rightAnchor="-13.0" AnchorPane.topAnchor="-4.0">
                    <columns>
                      <TableColumn fx:id="nameList" prefWidth="100.33334350585938" text="Nickname" />
                    </columns>
                     <columnResizePolicy>
                        <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
                     </columnResizePolicy>
                  </TableView>
               </children>
            </AnchorPane>
          <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
               <children>
                  <Label layoutX="49.0" layoutY="25.0" text="Person Details" AnchorPane.leftAnchor="5.0" AnchorPane.topAnchor="5.0" />
                  <GridPane accessibleText="erreererer" gridLinesVisible="true" layoutX="5.0" layoutY="31.0" AnchorPane.leftAnchor="5.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="31.0">
                    <columnConstraints>
                      <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
                      <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
                    </columnConstraints>
                    <rowConstraints>
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                     <children>
                        <Label prefHeight="17.0" prefWidth="70.0" text="Nickname" />
                        <Label text="Breed" GridPane.rowIndex="1" />
                        <Label text="Age" GridPane.rowIndex="2" />
                        <Label text="City" GridPane.rowIndex="3" />
                        <Label text="Level of training" GridPane.rowIndex="4" />
                        <TextField fx:id="breed" GridPane.columnIndex="1" GridPane.rowIndex="1" />
                        <TextField fx:id="age" GridPane.columnIndex="1" GridPane.rowIndex="2" />
                        <TextField fx:id="city" GridPane.columnIndex="1" GridPane.rowIndex="3" />
                        <TextField fx:id="levelOfTraining" GridPane.columnIndex="1" GridPane.rowIndex="4" />
                        <TextField fx:id="name" GridPane.columnIndex="1" />
                     </children>
                  </GridPane>
                  <Button layoutX="251.0" layoutY="259.0" mnemonicParsing="false" onAction="#create" text="New" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="230.0" AnchorPane.rightAnchor="130.0" AnchorPane.topAnchor="260.0" />
                  <Button layoutX="316.0" layoutY="262.0" mnemonicParsing="false" onAction="#edit" text="Edit" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="290.0" AnchorPane.rightAnchor="70.0" AnchorPane.topAnchor="260.0" />
                  <Button layoutX="360.0" layoutY="262.0" mnemonicParsing="false" onAction="#delete" text="Delete" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="350.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="260.0" />
               </children>
            </AnchorPane>
        </items>
      </SplitPane>
   </children>
</AnchorPane>
Друге (для створення нових песиків):

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tutorial.controller.NewDogController">
   <children>
      <GridPane layoutX="31.0" layoutY="25.0" prefHeight="122.0" prefWidth="412.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" maxWidth="185.0" minWidth="10.0" prefWidth="149.0" />
          <ColumnConstraints hgrow="SOMETIMES" maxWidth="173.0" minWidth="10.0" prefWidth="146.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label prefHeight="48.0" prefWidth="178.0" text="Please, write name:">
               <font>
                  <Font size="20.0" />
               </font>
            </Label>
            <TextField fx:id="nickName" prefHeight="36.0" prefWidth="173.0" GridPane.columnIndex="1" />
         </children>
      </GridPane>
      <Button layoutX="222.0" layoutY="149.0" mnemonicParsing="false" onAction="#ok" prefHeight="37.0" prefWidth="95.0" text="Save" />
      <Button layoutX="325.0" layoutY="149.0" mnemonicParsing="false" onAction="#cansel" prefHeight="37.0" prefWidth="95.0" text="Cansel" />
   </children>
</AnchorPane>
Як виглядає структура папок:Введення в Java FX - 9Як бачимо, нічого особливого: є контролери, які представляють певні віконця, є моделі, що представляють наші дані. Давайте поглянемо на клас, що запускає додаток (реалізація Application): @Data

public class AppFX extends Application {

    private Stage primaryStage;
    private AnchorPane rootLayout;
    private ObservableList listDog = FXCollections.observableArrayList();

    public AppFX() {
        listDog.add(new Dog("Fluffy", "Pug", 8, "Odessa", 2));
        listDog.add(new Dog("Archie", "Poodle", 3, "Lviv", 6));
        listDog.add(new Dog("Willie", "Bulldog", 5, "Kiev", 4));
        listDog.add(new Dog("Hector", "Shepherd", 9, "Minsk", 6));
        listDog.add(new Dog("Duncan", "Dachshund", 1, "Hogwarts", 9));
    }
Тут бачимо конструктор, який заповнюватиме наші початкові дані (які зберігаємо у спеціальному списку – ObservableList).

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("Dogs application");
        showBaseWindow();
}
Нічого особливого – main і реалізація start(), що запускає додаток:

public void showBaseWindow() {
        try {
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(AppFX.class.getResource("/maket/rootWindow.fxml"));
            rootLayout = loader.load();
            Scene scene = new Scene(rootLayout);
            primaryStage.setScene(scene);
            InputStream iconStream = getClass().getResourceAsStream("/icons/someImage.png");
            Image image = new Image(iconStream);
            primaryStage.getIcons().add(image);
            BaseController controller = loader.getController();
            controller.setAppFX(this);
            primaryStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
Отже, тут ми бачимо метод, який ми, власне, і запускаємо в start(), а саме – задає налаштування нашого базового вікна, такі як на XML-макеті в ресурсах: задає йому іконку, зв'язує його з конкретним контролером і задає контролеру посилання на клас this.

public void showCreateWindow(Dog dog) {
        try {
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(AppFX.class.getResource("/maket/new.fxml"));
            AnchorPane page = loader.load();
            Stage dialogStage = new Stage();
            dialogStage.setTitle("Wow Wow Wow");
            dialogStage.initModality(Modality.WINDOW_MODAL);
            dialogStage.initOwner(primaryStage);
            dialogStage.setScene(new Scene(page));
            CreateController controller = loader.getController();
            controller.setDialogStage(dialogStage);
            controller.setDog(dog);
            dialogStage.showAndWait();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Тут ми бачимо метод, який відповідає за появу другого вікна – вікна створення нового запису (імені нового собаки). Також задаємо контролер, XML-макет, stage та інше… Наступним розглянутим класом у нас буде модель, яка представляє нашу собаку (інфу про неї): @Data

public class Dog {

    private StringProperty name;
    private StringProperty breed;
    private IntegerProperty age;
    private StringProperty city;
    private IntegerProperty levelOfTraining;

    public Dog(String name, String breed, int age, String city, int levelOfTraining) {
        this.name = new SimpleStringProperty(name);
        this.breed = new SimpleStringProperty(breed);
        this.age = new SimpleIntegerProperty(age);
        this.city = new SimpleStringProperty(city);
        this.levelOfTraining = new SimpleIntegerProperty(levelOfTraining);
    }

    public Dog() {
        name = new SimpleStringProperty();
        breed = null;
        age = null;
        city = null;
        levelOfTraining = null;
    }
}
Тут ми бачимо два конструктори. Один – майже звичайний, з усіма аргументами (майже, тому що ми юзаємо спеціальні FX-оболонки простих типів), і конструктор без аргументів: його ми використовуємо при створенні нового собаки, який спочатку має тільки ім'я. Контролер для базового вікна: @Data

public class BaseController {

    @FXML
    private TableView dogs;
    @FXML
    private TableColumn nameList;
    @FXML
    private TextField name;
    @FXML
    private TextField breed;
    @FXML
    private TextField age;
    @FXML
    private TextField city;
    @FXML
    private TextField levelOfTraining;
    private AppFX appFX;
Тут бачимо наші поля об'єкта, але у форматі TextField. Це формат, який представляє поле для введення тексту. @FXML – анотація, призначена для зв'язування коду Java та відповідного об'єкта нашого макета (кнопки, поля або ще чогось).

@FXML
private void initialize() {
    nameList.setCellValueFactory(
            cellData -> cellData.getValue().getName());
    dogs.getSelectionModel().selectedItemProperty().addListener(
            (observable, oldValue, newValue) -> showDogsInformation(newValue));
}
Тут бачимо спосіб виведення імен собак праворуч у списку (його анотація @FXML пов'язує з компонентом макета JavaFX TableView).

public void setAppFX(AppFX appFX) {
    this.appFX = appFX;
    dogs.setItems(appFX.getListDog());
}

private void showDogsInformation(Dog dog) {
    if (dog != null) {
        name.setText(dog.getName() != null ? dog.getName().getValue() : null);
        breed.setText(dog.getBreed() != null ? dog.getBreed().getValue() : null);
        age.setText(dog.getAge() != null ? String.valueOf(dog.getAge().get()) : null);
        city.setText(dog.getCity() != null ? dog.getCity().getValue() : null);
        levelOfTraining.setText(dog.getLevelOfTraining() != null ? String.valueOf(dog.getLevelOfTraining().get()) : null);
    } else {
        name.setText("");
        breed.setText("");
        age.setText("");
        city.setText("");
        levelOfTraining.setText("");
    }
}
У першому методі ми бачимо задання внутрішнього посилання на клас, що реалізує Application (щоб можна було смикнути його метод виклику другого вікна), і задання початкового списку для відображення. Другий перевіряє, чи є певні дані поточного собаки, і на підставі цього задає текстові поля:

@FXML
    private void delete() {
        int selectedIndex = dogs.getSelectionModel().getSelectedIndex();
        dogs.getItems().remove(selectedIndex);
    }

    @FXML
    private void edit() {
        int selectedIndex = dogs.getSelectionModel().getSelectedIndex();
        dogs.getItems().set(selectedIndex, new Dog(name.getText(), breed.getText(), Integer.valueOf(age.getText()), city.getText(), Integer.valueOf(levelOfTraining.getText())));
    }

    @FXML
    private void create() {
        Dog someDog = new Dog();
        appFX.showCreateWindow(someDog);
        if (someDog.getName() != null && !someDog.getName().getValue().isEmpty()) {
            appFX.getListDog().add(someDog);
        }
    }
}
Тут ми бачимо три методи, базового вікна, пов'язані з кнопками:Введення в Java FX - 10
  • delete – за індексом видаляємо вибраного собаку;
  • edit – створюємо нового собаку з переданими даними і задаємо його замість того, який був до цього;
  • create – створюємо нового собаку і смикаємо метод виклику вікна створення, передавши новий об'єкт, і після його закриття, якщо ім'я не null, зберігаємо нового вихованця.
Рухаємося далі, контролер вікна для створення собаки: @Data

public class CreateController {
    private Stage dialogStage;
    private Dog dog;

    @FXML
    private TextField nickName;

    @FXML
    private void ok() {
        if (nickName != null && !nickName.getText().isEmpty()) {
            dog.setName(new SimpleStringProperty(nickName.getText()));
            dialogStage.close();
        }
    }

    @FXML
    private void cansel() {
        dialogStage.close();
    }
}
Тут ми бачимо зв'язок із текстовим полем у вікні, обробку кнопок Save та Cancel, які так чи інакше закривають вікно. Як ви бачите, для більшої зручності у своєму невеликому додатку я юзав Lombok, інакше код дуже сильно розрісся б, і в свій огляд я ніяк би його не вмістив. На цьому сьогодні в мене, мабуть, усе. Сьогодні ми коротко ознайомилися з базовими поняттями та прикладом використання JavaFX і тепер можемо будувати невеликі настільні додатки (використовуючи додаткову інфу, якої, добре, в інтернеті повно). А з вас, у свою чергу, – лайк))