16. Принципы разработки графического интерфейса. Фреймворк JavaFX

Для корректной работы JavaFX рекомендуется установить 8, 9 или 10 версию JDK.

Для работы JavaFX на с более поздними версиями Java требуется специальным образом создавать новый проект. Подробнее читайте по этой ссылке.

Отличие программы с графическим интерфейсом от консольной программы заключается не столько в способе подачи информации (использование графических объектов против текстового ввода и вывода информации), сколько в порядке выполнения (control flow) программного кода.

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

Такой порядок работы программы удобен, если рассматривать программу как математическую функцию или как решение какой-то математической задачи. Программа выполняет набор действий, а после того, как все действия выполнены, программы завершает свою работу.

Программа, использующая графический интерфейс пользователя (graphic user interface или GUI), работает иначе. Работа GUI-программы основана на событиях (event-driven; разработку программы с графическим интерфейсом иногда называют event-driver programming).

В данном случае событие - это изменение состояния объекта, которое может быть зафиксировано и обработано программой. Если говорить проще, событие - это:

  1. взаимодействие пользователя с компонентами графического интерфейса;

  2. выполнение некоторыми объектами определенного условия (например, сработал таймер, была выполнена некоторая операция);

  3. сообщения от операционной система (например, прерывания операционной системы, сбой аппаратного или программного обеспечения и так далее).

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

Среда Java (JVM) содержит механизмы, которые позволяют приложению реагировать на события - выполнять некоторые действия в ответ на нажатие кнопки, движение курсора, ввода символа из клавиатуры и так далее.

Каким образом приложение реагирует на события? Реакция на событие – это выполнение некоторого кода. Так как исполняемый код в ОО-языках хранится в методах, то реакция на событие, по сути, сводится к вызову определенного метода, который что-то делает. Таким образом, когда пользователь, например, нажимает на кнопку, то программа реагирует путем выполнения некоторого кода.

В Java существует огромное количество различных типов событий – даже самое незначительное действие может служить источником одного, а иногда и целой серии событий.

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

Схему работы GUI-приложения можно представить следующей схемой. Это очень упрощенная схема, но она дает понимание того, как работает GUI-приложение.

Вы можете обратить внимание на то, что вышеприведенная схема работы GUI-приложения напоминает цикл. На самом деле, это так и есть. Работу приложения можно, очень условно, разделить на три этапа:

  1. Подготовительный этап. На этом этапе программа создает необходимые объекты графического интерфейса (но пока их не показывает), устанавливает связи между объектами интерфейса и выполняет другую подготовительную работу. На этом этапе приложение не обрабатывает события и не взаимодействует с пользователем;

  2. Основной этап работы приложения. Приложение входит в бесконечный цикл (он называется event loop), в ходе которого, приложение на каждой итерации цикла проверяет – произошли ли какие-то события. Если есть произошедшие события – оно отправляет первое в очереди событие на обработку, на следующей итерации – следующее за ним в очереди событие, и так далее. Если нет произошедших событий – приложение просто пропускает эту итерацию;

  3. Завершение работы приложения. Приложение «крутится» в event loop до тех пор, пока не будет подана команда на закрытие приложения (пользователь выбрал пункт «Выйти из приложения», закрыл приложение из диспетчера задач, операционная система принудительно «убила» приложение и так далее). При поступлении команды на закрытие, приложение выходит из бесконечного цикла, после чего, оно может выполнить некоторые заключительные действия, после чего закрывается.

Основные библиотеки для разработки GUI на языке Java

Так как, согласно парадигме Java, всё является объектом, то и графический интерфейс, по сути, является набором объектов различных классов (кнопка, поле ввода, окно приложения – всё это является объектами соответствующих классов), который объединены в библиотеки графического интерфейса.

В Java нет никаких ограничений на реализацию своей библиотеки графического интерфейса – вы можете написать свою библиотеку, которая будет реализовывать свои классы компонентов графического интерфейса. Таким образом, наряду с библиотеками, которые были предложены разработчиками языка Java (AWT, Swing, JavaFX), существует множество библиотек, которые были разработаны сторонними фирмами или просто отдельными сообществами программистов (SWT, Apache Pivot и другие). Рассмотрим библиотеки, которые были предложены разработчиками языка Java.

AWT

AWT (Abstract Window Toolkit) – первая предложенная разработчиками библиотека GUI для Java. Главным недостатком данной библиотеки было то, что она использовала нативные компоненты ОС. Таким образом, внешний вид приложения, запущенного под ОС Window мог сильно отличаться от внешнего вида того же приложения, запущенного под OC Linux. На данный момент, эта библиотека считается устаревшей и не используется.

Swing

Swing – наиболее известная и вторая по счету библиотека GUI для Java. Обратите свое внимание на тот факт, что библиотека Swing активно использует компоненты AWT, наследуясь от них. Таким образом, хотя AWT и не используется напрямую, но Swing, во многом, опирается на библиотеку AWT.

Библиотека Swing является надежным проверенным средством для разработки GUI, а также имеет множество сторонних компонентов и классов, которые помогут вам в разработке. Недостатком Swing является то, что эта библиотека предназначена только для desktop-приложений и морально устарела, т.к. не поддерживает современные инструменты разработки графических интерфейсов (привязка данных, использование CSS и XML для описания интерфейса и так далее).

Разработчики языка Java, хотя и поддерживают библиотеку Swing, но настоятельно рекомендуют использовать библиотеку JavaFX.

JavaFX

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

Создание базового приложения с помощью JavaFX

Стартовый класс JavaFX-приложения должен расширять класс Application, который входит в состав пакета javafx.application

Компоненты JavaFX содержится в отдельных пакетах, имена которых начинаются с префикса javafx (например, javafx.application, javafx.stage, javafx.scene и так далее).

Точкой входа для JavaFX-приложений является базовый класс Application. Точка входа программиста вызывает метод класса Application, который запускает JavaFX-приложение.

Main.java
public class Main extends Application {

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

    @Override
    public void init() throws Exception {
        super.init();
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

    }

    @Override
    public void stop() throws Exception {
        super.stop();
    }
}

В классе Application важными для нас являются три метода (они называются методами жизненного цикла, т.к. они вызываются системой в определенный момент «жизни» приложения):

  1. метод init() вызывается в момент, когда приложение только начинает выполняться. Он служит для выполнения различных инициализаций. Если инициализация не требуется – просто не переопределяйте данный метод;

  2. метод start() вызывается после init(). Этот метод в классе Application является абстрактным, поэтому нам необходимо его переопределить. Именно с него начинается работа приложения. Здесь создаются и настраиваются компоненты графического интерфейса;

  3. метод stop() вызывается, когда приложение завершается. Именно в нем должны быть произведены все операции очистки или закрытия. Если это не требуется – просто не переопределяем метод.

Когда запускается приложение, JavaFX выполняет следующие действия:

  1. создает объект класса, который наследуется от класса Application;

  2. вызывает метод init();

  3. вызывает метод start();

  4. ждет завершения приложения. Приложение может завершиться в следующих случаях:

    1. приложение вызвало метод Platform.exit();

    2. закрыто последнее окно и атрибут implicitExit класса Platform равен true;

  5. вызывает метод stop().

Запуск JavaFX-приложения

Для того чтобы запустить JavaFX-приложение на выполнение, следует вызвать статический метод launch(), определяемый в классе Application.

Вызов метода launch() приводит к построению приложения и последующему вызову методов init() и start(). Возврат из метода launch() не происходит до тех пор, пока приложение не завершится.

Классы Stage и Scene

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

Центральным понятием в JavaFX является Stage (во многих книгах этот переводится как «подмостки», имеется ввиду «театральные подмостки»). Класс Stage является нашей театральной сценой, подмостками, основным контейнером, который, как правило, представляет собой обрамлённое окно со стандартными кнопками: закрыть, свернуть, развернуть. Внутри Stage содержится сцена – объект класса Scene, которая может быть заменена другим объектом класса Scene. Объект класса Stage может содержать одновременно только один объект класса Scene.

Объект класса Scene содержит в один элемент – специальную вершину, которая называется корневая вершина (root node). Это самый верхний и единственный узел графа, не имеющий родителя. Все узлы в сцене прямо или косвенно происходят от корневого узла. В качестве корневого узла может выступать любой элемент графического интерфейса, который наследуется от абстрактного класса Parent.

Корневой узел включает в себя множество различных графических компонент – кнопок, полей, переключателей, надписей – всё то, что нам нужно для создания графического интерфейса. На первом этапе думайте о Stage просто как об окне приложения, а о Scene – как о полотне, на котором размещаются нужные вам элементы. Все элементы графического интерфейса будут добавляться в корневую вершину.

В рамках одного окна вы можете менять объекты Scene и, таким образом, менять содержимое окна. Вы также можете создать другие объекты Stage – когда вам нужно открыть другие окна в приложении. Стартовый объект Stage (аргумент primaryStage) создает за вас JavaFX и передает вам как аргумент метода start().

Main.java
public class Main extends Application {

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

    @Override
    public void init() throws Exception {
        super.init();
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        // Устанавливаем заголовок окна
        primaryStage.setTitle("JavaFX приложение");
        // Создаем root node
        FlowPane rootNode = new FlowPane();
        // Создаем новую сцену и добавляем туда корневую вершину
        Scene scene = new Scene(rootNode, 300, 200);
        // Добавляем сцену в primaryStage
        primaryStage.setScene(scene);

        // Метод show() делает окно видимым на экране
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        super.stop();
    }
}

Узлы и графы сцены

Отдельные элементы сцены называются узлами. Например, кнопка, поле ввода, надпись и так далее. Некоторые узлы могут содержать в себе другие узлы (например, группа радиокнопок). Совокупность всех узлов сцены называется графом сцены, который образует дерево. Базовым классом для всех узлов служит класс Node. От Node, прямо или косвенно, происходят другие классы, например Parent, Group, Region и Control.

Если узел может иметь потомков, тогда он называется узлом ветвления (branch node), если у узла не может быть потомков, он называется листом (leaf node). Узлы ветвления наследуются от класса Parent.

Рассмотрим небольшой пример. Построим графический интерфейс в виде панели с тремя надписями.

В качестве панели будет использоваться компонент HBox. Этот компонент является менеджером компоновки (Layout Manager) – специальным контейнером, в который можно помещать элементы, которые будут расположены в определенном порядке.

Компонент HBox размещает содержащиеся в нем элементы в виде горизонтального ряда. Такой компонент, очевидно, является узлом ветвления, так как он может иметь детей. Текстовая надпись (класс Text), очевидно, является листом, т.к. вы не можете ничего поместить в текстовую надпись.

Граф сцены для вышеприведенного приложения будет следующим:

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

Main.java
public class Main extends Application {

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

    @Override
    public void init() throws Exception {
        super.init();
    }

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

        // Контейнер для текстовых надписей
        HBox rootNode = new HBox();
        // Расстояние между элементами
        rootNode.setSpacing(40);
        // Padding корневой вершины
        rootNode.setPadding(new Insets(40));

        // Объекты текстовых надписей
        Text text1 = new Text("Текст1");
        Text text2 = new Text("Текст2");
        Text text3 = new Text("Текст3");

        // Добавление текстовых надписей в корневую вершину
        rootNode.getChildren().addAll(text1, text2, text3);

        Scene scene = new Scene(rootNode);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


    @Override
    public void stop() throws Exception {
        super.stop();
    }
}

Менеджеры компоновки

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

Для того, чтобы самостоятельно не заниматься отслеживанием таких ситуаций, во многих библиотеках графического интерфейса используются специальные компоненты, которые называются менеджерами компоновки (Layout Manager).

Менеджеры компоновки определяют размер и расположение компонентов, а так же, при изменении размера окна пропорционально масштабируют компоненты формы.

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

BorderPane

Класс располагает свои дочерние узлы в одном из пяти регионов: top, bottom, left, right, center. Эти регионы могут быть любых размеров. Если не поместить ни одного элемента в какой-либо регион, для него не будет выделено пространство.

Менеджер BorderPane используется, когда необходимо достичь классического вида приложения – с панелью инструментов вверху, статусной строкой внизу, навигационной панелью слева, дополнительной информацией справа и рабочей областью в центре. Например:

Рассмотрим небольшой пример

HBox

Менеджер разметки HBox позволяет легко упорядочить элементы в горизонтальный ряд. С помощью метода setPadding() можно установить расстояние между узлами и краями HBox. С помощью метода setSpacing() можно регулировать расстояние между узлами. Менеджер поддерживает установку стиля.

VBox

Менеджер разметки VBox работает схожим с HBox образом, за исключением того, что узлы упорядочены в вертикальный ряд.

StackPane

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

GridPane

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

Обратите внимание на то, как в разметке GridPane задаются свойства отдельных колонок и рядов: создается объект класса ColumnConstraints и RowConstrains соответственно, в объектах устанавливаются нужные свойства, после чего этот объект передается GridPane (также, это можно сделать, указав индекс колонки или ряда).

FlowPane

FlowPane – класс поточной компоновки. Элементы в этой компоновке располагаются построчно с автоматическим переходом на новую строку, если требуется.

Рассмотрим пример:

Попробуем изменять размер окна и увидим, что если кнопки не помещаются в окно по ширине, то кнопки автоматически переносятся на следующую строку.

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

TilePane

Работа этого менеджера компоновки схожа с работой FlowPane, отличие в том, что дочерние узлы помещаются в сетку, ячейки которого имеют одинаковый размер.

Реализуем предыдущий пример, но сделаем кнопку 3 больше остальных. Обратите внимание на отличия между работой FlowPane и TilePane

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

AnchorPane

Разметка AnchorPane позволяет привязывать элементы к верхнему, нижнему, правому, левому краям контейнера. При изменении размеров окна, узлы поддерживают позицию относительно привязки. Узлы могут быть привязаны к нескольким позициям, и к одной позиции может быть привязано более одного узла

Group

Особняком в списке классов-контейнеров стоит класс Group. Элементы в Group не привязываются к местоположению, их позиция задается в абсолютных значениях. Если не задано другое, все дочерние узлы группы позиционируются по координате (0,0) (верхний левый угол).

Компонент Group часто используют для того, чтобы применить различные эффекты и преобразования к целому набору компонентов, которые находятся в группе. Размеры компонента Group определяются размерами его потомков.

Попробуем добавить пару кнопок в группу

@Override
public void start(Stage primaryStage) throws Exception {

    // Заголовок окна
    primaryStage.setTitle("JavaFX приложение");

    Group root_node = new Group();

    Button button1 = new Button("Кнопка 1");
    button1.setPrefHeight(90);
    button1.setPrefWidth(160);

    Button button2 = new Button("Кнопка 2");

    root_node.getChildren().addAll(button1, button2);

    Scene scene = new Scene(root_node);
    primaryStage.setScene(scene);
    primaryStage.show();
}

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

Попробуем установить для кнопок позицию в группе. Мы видим, что кнопки расположились согласно координатам.

@Override
public void start(Stage primaryStage) throws Exception {

    // Заголовок окна
    primaryStage.setTitle("JavaFX приложение");
    Group root_node = new Group();
    
    Button button1 = new Button("Кнопка 1");
    button1.setLayoutX(20);
    button1.setLayoutY(20);

    Button button2 = new Button("Кнопка 2");
    button2.setLayoutX(70);
    button2.setLayoutY(75);

    root_node.getChildren().addAll(button1, button2);

    Scene scene = new Scene(root_node);
    primaryStage.setScene(scene);
    primaryStage.show();
}

Полезным свойством Group является возможность устанавливать эффекты и преобразований для всей группы. На примере ниже мы устанавливаем эффект BoxBlur и этот эффект автоматически применяется ко всем узлам внутри группы.

@Override
public void start(Stage primaryStage) throws Exception {

    // Заголовок окна
    primaryStage.setTitle("JavaFX приложение");
    Group root_node = new Group();

    Button button1 = new Button("Кнопка 1");
    button1.setLayoutX(20);
    button1.setLayoutY(20);

    Button button2 = new Button("Кнопка 2");
    button2.setLayoutX(70);
    button2.setLayoutY(75);

    root_node.getChildren().addAll(button1, button2);

    // **** Наложение эффекта
    root_node.setEffect(new BoxBlur(5,5,1));

    Scene scene = new Scene(root_node);
    primaryStage.setScene(scene);
    primaryStage.show();
}

Создание меню приложения

За создание меню в JavaFX отвечают несколько классов:

  • MenuBar – панель меню;

  • Menu – раздел меню (Главная, Вставка и т.д.);

  • MenuItem – действие в пункте меню;

  • RadioButtonItem – для создания пункта меню в виде RadioButton (выбор одного из нескольких вариантов);

  • CheckMenuItem – создание пункта меню в виде CheckBox (выделение\снятие выделения);

  • SeparatorMenuItem - для отделения одной категории от другой.

В JavaFX нет ограничений на расположение панели меню, однако, как правило, панель меню располагается вверху окна. Панель меню добавляется как обычный узел, наряду с другими компонентами графического интерфейса. Панель меню содержит один или несколько разделов. Каждый раздел в панели выглядит как кнопка с текстовым значением. Рассмотрим небольшой пример

При нажатии на раздел меню ничего не происходит, т.к. мы добавили пункты меню. Давайте добавим разнообразные пункты меню в раздел «Файл» и посмотрим на результат

Пример разработки графического интерфейса

В качестве примера воспроизведем интерфейс приложения «Калькулятор» для Windows 7.

Для начала, нам необходимо понять – какие менеджеры компоновки нам следует использовать. При разработке GUI желательно использовать как можно меньшую вложенность менеджеров разметок и как можно меньший граф сцены.

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

Следовательно, здесь уместным будет использование менеджера разметки VBox, который размещает элементы в вертикальный список. Элемент класса VBox будет нашим корневым узлом графа (root node).

Создадим метод configureMenu(), в котором мы будем иницализировать и настраивать объекты меню.

Разделы меню мы заполнять не будем, а пока что попробуем воспроизвести окно результата. Создадим метод configureResultView(), в котором мы будем создавать и настраивать объекты для реализации окна показа результата.

Текстовое поле (класс Text) само по себе имеет не очень много возможностей для настройки выравнивания и внешнего вида. Поэтому, чтобы добиться сходства с оригинальным калькулятором, поместим текстовое поле в HBox, а уже HBox поместим как второй дочерний узел для VBox.

Теперь перейдем к третьему элементу VBox, в котором будут содержаться кнопки. Для того, чтобы выбрать нужную разметку, давайте внимательно посмотрим на область с кнопками.

Нетрудно догадаться, что это разметка типа Grid – то есть, сетка с набором кнопок. Две кнопки («=» и «0» занимают две строчки и два столбца соответственно). Таким образом, используем менеджер GridPane. Создадим метод configureButtons(), в котором будем создавать кнопки и помещать их в соответствующие ячейки сетки.

Для начала необходимо настроить кнопки. По умолчанию, размер кнопки определяется ее содержимым. Нам же необходимо будет, чтобы кнопка могла растягиваться в ширину и высоту, чтобы растягиваться в нашей сетке. Лучшим вариантом будет – создать собственный класс, который наследуется от стандартной кнопки и в конструкторе собственного класса вызвать метод setMaxSize()

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

Далее, создадим объект класса ColumnConstraints, после чего передадим объект этого классов для каждой колонки нашей таблицы. В этом объекте мы указываем, что колонка должна максимально расширяться. Также, установить отступы и зазоры между ячейками таблицы.

Полный текст класса представлен в конце лекции

Last updated