JavaRush /Java блог /Random UA /Введення в Java FX
Константин
36 рівень

Введення в Java FX

Стаття з групи Random UA
Якось у мене виникла ідея написати невеликий настільний додаток для своїх потреб — щось на кшталт невеликого словника для вивчення іноземних слів — і я почав ламати голову, а як би мені це зробити? Звичайно, перше, що мені спало на думку - 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… Він простий у освоєнні та добре оптимізований. Він підтримує багато операційних систем, а також Windows, UNIX системи та Mac OS. Введення в Java FX - 3

Особливості 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 - 4 Отже, давайте рухатись трохи у бік коду. Так як у мене юзается 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(що у нас з коробки Бугага). У мейні викликаємо статичний метод 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 - 58 - створюємо файл як об'єкт Image, який пов'язаний з реальним файлом потоком передається в конструкторі 9 - задаємо іконку у верхню панель вікна 11 - створюємо об'єкт кнопки 13-16 - задаємо реакцію при натисканні кнопки 17 - створюємо сцену, куди поміщаємо нашу кнопку 18 - сцену поміщаємо на наше загальне вікно 20 - задаємо прапор видимості для вікна І як результат віконце, для вітання наших улюблених песелів: Введення в Java FX - 6Все виглядає в рази простіше, ніж 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 - 7

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

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

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

Деталі установки я упущу, і докладне вивчення цього інструменту теж. Це теми, які варто досліджувати додатково. Тому все ж таки залишу пару цікавих посилань на JavaFX уроки: раз (онлайн підручник з JavaFX) і два (ще один непоганий туторіал). Давайте трохи пробіжимося невеликим прикладом, який я накидав. У результаті у мене вийшло щось на кшталт: Введення в Java FX-9
(таке собі віконце для обліку собак)
При виборі песеля та натисканні кнопки Delete собака видаляється з нашого списку. При виборі чотирилапого друга і зміні його полів, а після натискання кнопки Edit - инфа собачки оновлюється. Коли натискаємо кнопку New, вилазить віконце для створення запису нового собаки (для початку його імені): Введення в Java FX - 10Після тиснемо 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 - 11Як бачимо, нічого особливого, є контролери, які представляють певні віконця, є моделі, що представляють наші дані. Давайте поглянемо на клас, що запускає додаток (реалізація 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 - 12
  • 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, і можемо будувати невеликі настільні додатки (використовуючи додаткову інфу, якій, добре, в інтернетах повно). А з вас, у свою чергу, лайк))
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ