Программирование JavaFX: разработка элементов интерфейса

pa_kd7bt8qvf9mytls2ukmadgrm.jpeg


Иногда даже программистам на Java необходимо создавать интерфейсы, и для этого им приходится изучать дополнительные инструменты. В этом случае им на помощь приходит инструментарий создания GUI, который избавляет от необходимости подключения дополнительных технологий — JavaFX.

Нужно сразу оговориться и сказать о том, что этот фреймворк — достаточно специфический и нишевый продукт, так как в настоящее время для создания интерфейсов обычно предпочитают использовать веб-подход и делать браузерные интерфейсы. Но минус этого подхода в том, что вам придётся разбираться в дополнительных технологиях, например, в том же самом JavaScript. В случае же с JavaFX вы продолжаете находиться в рамках знакомых вам технологий и сразу «визуально оживляете» их, что даже само по себе приятно наблюдать.

То есть в случае веб-подхода вы бы использовали, например, HTML, CSS, JavaScript; в случае же с JavaFX эта связка будет выглядеть как FXML, CSS, Java. Это означает, что на выходе мы получим десктопное приложение.

Что нужно для работы


Для работы с JavaFX вам понадобится установить две вещи: JDK Java и SDK JavaFX. Почему раздельно? Раньше всё это было несколько проще, и JavaFX шла в комплекте с основным JDK Java, сейчас же этот фреймворк распространяется в виде отдельного модуля, и вам надо будет скачать его отдельно.

Сказанное выше касается только тех ситуаций, когда вы не используете ранние версии Java вплоть до 10, так как в них JavaFX уже содержится в комплекте JDK.

Если же вы используете версию 11+, то у вас есть два варианта: либо качать SDK JavaFx и устанавливать зависимости вручную, либо воспользоваться любой системой сборки, например, Maven. Кстати говоря, если вы будете использовать именно Maven, то вам даже не придётся качать JavaFX, так как он загрузит необходимые модули самостоятельно.

Вкратце пару слов про системы сборки и для чего это надо: в настоящее время, как правило, те компьютеры, на которых происходит разработка приложения, и те компьютеры, которые будут запускать приложения, — это разные машины.

Соответственно, после того как приложение было создано, для корректной работы требуется перенести всю структуру его папок и файлов на другой компьютер. Установить библиотеки, зависимости файлов друг от друга и очерёдность сборки. Можно это делать и вручную, но это будет «долго и печально», либо же использовать системы сборки, которые автоматически соберут проект для переноса. Соответственно, эти системы сборки должны знать, где и что у вас лежит, а также ряд другой информации. Именно под этим и подразумевается понятие «зависимостей».

С понятием зависимостей неразрывно связано и понятие архетипов, под которым подразумевается определённая конфигурация настройки. То есть это своего рода шаблон конфигурации, который можно применить к вашему проекту. Имея разные типы шаблонов, вы можете создавать различные типы проектов.

По ссылке выше (где описана установка с применением Maven) как раз говорится о настроенных архетипах для ускорения процесса создания проектов, которые предлагает разработчик JavaFX.

Например, простой проект:

mvn archetype:generate \
        -DarchetypeGroupId=org.openjfx \
        -DarchetypeArtifactId=javafx-archetype-simple \
        -DarchetypeVersion=0.0.3 \
        -DgroupId=org.openjfx \
        -DartifactId=sample \
        -Dversion=1.0.0 \
        -Djavafx-version=17.0.1


Структура приложений


Изначально в составе поставки JavaFX имеет большой инструментарий разнообразных элементов управления, таблиц и т.д, что облегчает работу с ним.

Если говорить о структуре типичного приложения, то оно представляет собой своеобразную матрёшку, представленную тремя уровнями: Stage, Scene, Node:

image
Источник картинки: Education wiki

То есть Stage — это то, что вмещает всё содержимое, внутри него находится как минимум один элемент Scene.

Все элементы Scene хранятся в виде так называемого Scene Graph, то есть в виде иерархии элементов.

На рисунке выше Node — это управляющие элементы, в качестве которых могут выступать как отдельные кнопки, так и макеты. Кроме того, может присутствовать вложенность одних элементов в другие.

Говоря об узле, можно увидеть, что на картинке он обозначен тремя идентификаторами: ROOT, BRANCH, LEAF.

  • ROOT — самый первый узел, который называется также первым графом сцены.
  • BRANCH — узел, у которого имеются производные дочерние узлы.
  • LEAF — конечный узел, у которого нет никаких дочерних. В качестве подобного примера можно назвать такие, как, например, box, rectangle.


Например, приложение со всеми тремя уровнями может выглядеть вот так:

image
Источник картинки: Vojtechruzicka

На картинке показано несколько Scene, так как мы уже говорили выше, что они могут переключаться между собой.

Чтобы проиллюстрировать всё вышесказанное, обратимся к документации, несмотря на то, что она относится к 8 версии, многое ещё применимо и в новых версиях. Например, нам необходимо создать простое приложение, которое выводит на экран какую-то фразу:

image
Источник картинки: Oracle

Если мы попробуем представить структуру этого приложения в виде схемы (Scene Graph, о котором мы уже говорили выше), то выглядеть это будет вот так:

image
Источник картинки: Oracle

Соответственно, код этого примера будет выглядеть следующим образом:

package helloworld;
 
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
 
public class HelloWorld extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        Button btn = new Button();
        btn.setText("Say 'Hello World'");
        btn.setOnAction(new EventHandler() {
 
            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });
        
        StackPane root = new StackPane();
        root.getChildren().add(btn);

 Scene scene = new Scene(root, 300, 250);

        primaryStage.setTitle("Hello World!");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
 public static void main(String[] args) {
        launch(args);
    }
}


Как вы видите, код создаёт кнопку, при нажатии на которую в консоль выводится надпись.

А допустим, нам необходимо некоторым образом организовать управляющие элементы, для этого служат, например, такие классы, как TilePane, GridPane, StackPane, BorderPane, AnchorPane, FlowPane, VBox, HBox:

image
Источник картинки: Metanit

Все эти панели компоновки входят в пакет javafx.scene.layout, описание классов которого находится по этому адресу:

  • TilePane — элементы в сетке из «плиток» одинакового размера.
  • GridPane — элементы в гибкой сетке строк и столбцов.
  • StackPane — элементы в стеке в обратном порядке.
  • BorderPane — элементы сверху, слева, справа, снизу и по центру.
  • AnchorPane — позволяет привязывать края дочерних узлов к смещению от краёв области привязки.
  • FlowPane — элементы в потоке, который обтекает границу области потока.
  • VBox — элементы в одном вертикальном столбце.
  • HBox — элементы в один горизонтальный ряд.


Например, рассмотрим GridPane, который создаёт таблицу и размещает элементы содержимого в ячейках этой таблицы. Описание этого класса находится здесь.

С применением такой компоновки можно создать достаточно красивые и сложные формы, например, регистрационную форму, подробный процесс создания которой рассмотрен здесь:

image
Источник картинки: Callicoder

Создание и настройка GridPane происходит вот таким образом:

    // Создание новой панели сетки                                    
    GridPane gridPane = new GridPane();

    // Положение панели в центре экрана как по вертикали, так и по горизонтали                                   
    gridPane.setAlignment(Pos.CENTER);

    // Установка отступа по 20 пикселей с каждой стороны                                   
    gridPane.setPadding(new Insets(40, 40, 40, 40));

    // Установка горизонтального зазора между столбцами                                   
    gridPane.setHgap(10);

    // Установка вертикального зазора между строками                                   
    gridPane.setVgap(10);

    // columnOneConstraints будет применяться ко всем узлам, размещённым в первом столбце                                   
    ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE);
    columnOneConstraints.setHalignment(HPos.RIGHT);

    // columnTwoConstraints будет применяться ко всем узлам, размещённым во втором столбце                                   
    ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE);
    columnTwoConstrains.setHgrow(Priority.ALWAYS);

    gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains);


Полный код примера выше
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.stage.Window;

public class RegistrationFormApplication extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Registration Form JavaFX Application");

        // Create the registration form grid pane
        GridPane gridPane = createRegistrationFormPane();
        // Add UI controls to the registration form grid pane
        addUIControls(gridPane);
        // Create a scene with registration form grid pane as the root node
        Scene scene = new Scene(gridPane, 800, 500);
        // Set the scene in primary stage    
        primaryStage.setScene(scene);
        
        primaryStage.show();
    }


    private GridPane createRegistrationFormPane() {
        // Instantiate a new Grid Pane
        GridPane gridPane = new GridPane();

        // Position the pane at the center of the screen, both vertically and horizontally
        gridPane.setAlignment(Pos.CENTER);

        // Set a padding of 20px on each side
        gridPane.setPadding(new Insets(40, 40, 40, 40));

        // Set the horizontal gap between columns
        gridPane.setHgap(10);

        // Set the vertical gap between rows
        gridPane.setVgap(10);

        // Add Column Constraints

        // columnOneConstraints will be applied to all the nodes placed in column one.
        ColumnConstraints columnOneConstraints = new ColumnConstraints(100, 100, Double.MAX_VALUE);
        columnOneConstraints.setHalignment(HPos.RIGHT);

        // columnTwoConstraints will be applied to all the nodes placed in column two.
        ColumnConstraints columnTwoConstrains = new ColumnConstraints(200,200, Double.MAX_VALUE);
        columnTwoConstrains.setHgrow(Priority.ALWAYS);

        gridPane.getColumnConstraints().addAll(columnOneConstraints, columnTwoConstrains);

        return gridPane;
    }

    private void addUIControls(GridPane gridPane) {
        // Add Header
        Label headerLabel = new Label("Registration Form");
        headerLabel.setFont(Font.font("Arial", FontWeight.BOLD, 24));
        gridPane.add(headerLabel, 0,0,2,1);
        GridPane.setHalignment(headerLabel, HPos.CENTER);
        GridPane.setMargin(headerLabel, new Insets(20, 0,20,0));

        // Add Name Label
        Label nameLabel = new Label("Full Name : ");
        gridPane.add(nameLabel, 0,1);

        // Add Name Text Field
        TextField nameField = new TextField();
        nameField.setPrefHeight(40);
        gridPane.add(nameField, 1,1);


        // Add Email Label
        Label emailLabel = new Label("Email ID : ");
        gridPane.add(emailLabel, 0, 2);

        // Add Email Text Field
        TextField emailField = new TextField();
        emailField.setPrefHeight(40);
        gridPane.add(emailField, 1, 2);

        // Add Password Label
        Label passwordLabel = new Label("Password : ");
        gridPane.add(passwordLabel, 0, 3);

        // Add Password Field
        PasswordField passwordField = new PasswordField();
        passwordField.setPrefHeight(40);
        gridPane.add(passwordField, 1, 3);

        // Add Submit Button
        Button submitButton = new Button("Submit");
        submitButton.setPrefHeight(40);
        submitButton.setDefaultButton(true);
        submitButton.setPrefWidth(100);
        gridPane.add(submitButton, 0, 4, 2, 1);
        GridPane.setHalignment(submitButton, HPos.CENTER);
        GridPane.setMargin(submitButton, new Insets(20, 0,20,0));

        submitButton.setOnAction(new EventHandler() {
            @Override
            public void handle(ActionEvent event) {
                if(nameField.getText().isEmpty()) {
                    showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your name");
                    return;
                }
                if(emailField.getText().isEmpty()) {
                    showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter your email id");
                    return;
                }
                if(passwordField.getText().isEmpty()) {
                    showAlert(Alert.AlertType.ERROR, gridPane.getScene().getWindow(), "Form Error!", "Please enter a password");
                    return;
                }

                showAlert(Alert.AlertType.CONFIRMATION, gridPane.getScene().getWindow(), "Registration Successful!", "Welcome " + nameField.getText());
            }
        });
    }

    private void showAlert(Alert.AlertType alertType, Window owner, String title, String message) {
        Alert alert = new Alert(alertType);
        alert.setTitle(title);
        alert.setHeaderText(null);
        alert.setContentText(message);
        alert.initOwner(owner);
        alert.show();
    }

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


Говоря об управляющих элементах (которые, как вы видели, использовались выше), например, о таких, как кнопки, мы фактически говорим об узле (Node). Примерный вид иерархии классов, которые наследуются от Node, можно увидеть на картинке ниже:

image
Источник картинки: Metanit

Полный же список элементов, где так или иначе используется Node, можно посмотреть здесь.

Говоря о тексте, который используется в различных элементах сцены, можно сказать, что к текстовым элементам возможно применять различные эффекты, так как они наследуются от Node.

Причём, учитывая, что сам этот класс наследуется от Shape, текстовые элементы могут содержать обводку или любую заливку, так же как и обычная фигура. Примеры подобной работы с текстом имеются здесь:

image
Источник картинки: Oracle

Код текста выше:

String family = "Helvetica";
double size = 50;

TextFlow textFlow = new TextFlow();
textFlow.setLayoutX(40);
textFlow.setLayoutY(40);
Text text1 = new Text("Hello ");
text1.setFont(Font.font(family, size));
text1.setFill(Color.RED);
Text text2 = new Text("Bold");
text2.setFill(Color.ORANGE);
text2.setFont(Font.font(family, FontWeight.BOLD, size));
Text text3 = new Text(" World");
text3.setFill(Color.GREEN);
text3.setFont(Font.font(family, FontPosture.ITALIC, size));
textFlow.getChildren().addAll(text1, text2, text3);

Group group = new Group(textFlow);
Scene scene = new Scene(group, 500, 150, Color.WHITE);
stage.setTitle("Hello Rich Text");
stage.setScene(scene);
stage.show();


Богатые возможности JavaFx по созданию графических элементов позволяют делать, например, такие элементы, как круговые диаграммы, столбчатые и т.д.:

image
Источник картинки: Oracle

Говоря о графическом наполнении, нельзя не отметить, что в развитие фреймворка серьёзный вклад привносит сообщество энтузиастов, сложившееся вокруг этой технологии. Если взять те же самые диаграммы, одна из команд разработчиков предложила библиотеку, которая позволяет создавать диаграммы достаточно впечатляющего вида:

image
Источник картинки: Github HanSolo

image
Источник картинки: Github HanSolo

image
Источник картинки: Github HanSolo

Или, например, библиотека по генерации разнообразных часов и индикаторов:

imageИсточник картинки: Github HanSolo

Другие интересные и впечатляющие примеры использования библиотек для создания полезных приложений вы можете найти в разделе Community.

Завершая этот рассказ, следует отметить, что мы рассмотрели ключевые моменты этого фреймворка лишь обзорно и «глубоко не углубляясь». Поэтому в рамках этого рассказа не был рассмотрен графический способ создания интерфейсов с помощью Scene Builder, а также мы не затронули использование специфической реализации XML для JavaFX — FXML.

Обо всём этом мы поговорим в следующий раз.

НЛО прилетело и оставило здесь промокод для читателей нашего блога:

— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

© Habrahabr.ru