Лекция 13

Тема: Принципы разработки GUI. Библиотека JavaFX. Создание базового приложения с помощью 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, после чего передадим объект этого классов для каждой колонки нашей таблицы. В этом объекте мы указываем, что колонка должна максимально расширяться. Также, установить отступы и зазоры между ячейками таблицы.

Обработка событий играет важную роль, поскольку большинство элементов управления GUI генерируют события, которые обрабатываются программой. Когда вы используете кнопки, флажки или списки, все они генерируют события.

Базовым классом событий JavaFX является класс Event, находящийся в пакете javafx.event. Класс Event наследует класс java.util.eventObject, а это значит, что события JavaFX разделяют общую функциональность с другими событиями Java. Для класса Event определено несколько подклассов, каждый из которых отвечает за свой тип событий.

JavaFX различает много видов событий, среди которых, например, DragEvent, KeyEvent, MouseEvent, ScrollEvent и другие.

Чтобы среагировать на нажатие кнопки, вам необходимо написать код для обработки этого события. В данном случае, кнопка называется источник события (event source object) – объект, где произошло событие.

Само по себе событие является объектом соответствующего класса (наследуемого от javafx.event) и называется объектом события (event object). Чтобы обработать событие, необходимо создать объект, который может обработать это событие. Такой объект называется обработчик событий (event handler).

Не все объекты могут быть обработчиками события. Чтобы быть обработчиком action event, необходимо выполнить два требования:

  1. объект должен быть экземпляром класса, который реализует интерфейс EventHandler<T extends Event>. Этот интерфейс определяет общее поведение для всех обработчиков. Как мы помним, <T extends Event> означает, что T – обобщенный тип, который является классом или подклассом Event;

  2. обработчик события должен быть зарегистрирован источником события с помощью метода setOnAction().

Интерфейс EventHandler<T extends Event> содержит метод handle(T event) для обработки события. Ваш обработчик должен переопределить метод, чтобы среагировать на событие. Рассмотрим небольшой пример

На данный момент вы уже должны понимать содержимое метода start(). Если мы нажмем на одну из кнопок, то ничего не произойдет, т.к. по умолчанию, никакой реакции на нажатие не предусмотрено – реакцию должны прописать мы.

Для этого мы должны создать объект класса, который реализует интерфейс EventHandler<T extends Event> и передать этот объект кнопке. Передать кнопке объект обработчика событий мы можем двумя способами:

  1. использовать метод addEventHandler(), которому нужно передать тип события, а также объект обработчика события;

  2. использовать один из множества методов setOn***(), каждый из которых позволяет установить обработчик для того или иного события.

Событие нажатия на кнопку относится к типу ActionEvent. Для каждого элемента GUI событие ActionEvent означает свой тип события, как правило, событие центральное, наиболее значимое для этого элемента. Например, для кнопки основное событие – когда на нее нажали. Именно за это нажатие и отвечает тип ActionEvent.

Используем для каждой кнопки один из способов задания обработчика события

Результат работы выглядит следующим образом:

Важное отличие двух способов состоит в том, что в случае использования метода addEventHandler() мы можем назначить более одного обработчика на то или иное событие. Например, в следующем примере будут выполнены сразу два метода двух обработчиков

Механизм обработки событий

Кратко, описать механизм обработки событий можно следующим образом: в JavaFX обработка событий выполняется по цепочке диспетчеризации событий. Когда событие генерируется, оно передается сначала корневому узлу, а затем вниз по цепочке адресату события. После обработки события в узле адресата, оно передается обратно вверх по цепочке, предоставляя возможность родительским узлам обработать его по мере надобности. Такой механизм распространения событий называется всплыванием событий. Он позволяет любому узлу цепочки «поглотить» (consume) событие, чтобы оно больше не обрабатывалось.

Теперь разберемся более подробно.

Каждое событие (объект класса javafx.event.Event либо его подкласса) имеет три свойства:

  • тип события (event type);

  • источник события (event source);

  • цель события (event target).

Соответственно, класс Event предоставляет методы, общие для всех объектов события

Метод

Описание

getSource()

Возвращает источник события типа Object

getTarget()

Возвращает цель события типа Object

getEventType()

Возвращает тип события типа EventType

Подклассы класса Event содержат дополнительную информацию, которая описывают конкретный тип события. Например, класс MouseEvent определяет методы getX() и getY(), которые возвращает координаты курсора мыши относительно источника события.

Тип события.

Тип события является экземпляром класса EventType. Все типы событий являются экземплярами этого класса. Например, класс KeyEvent содержит следующие типы событий:

  • KEY_PRESSED;

  • KEY_RELEASED;

  • KEY_TYPED.

Типы событий организованы в иерархию. Каждый тип событий имеет имя и «супер-тип». Например, имя типа, который отвечает за нажатие кнопки клавиатуры называется KEY_PRESSED, а супер-типом является KeyEvent.ANY.

Самым верхним уровнем типа событий в иерархии является Event.ROOT, который идентичен типу Event.ANY. Тип событий ANY означает любое событие этого типа. К примеру, указав тип события KeyEvent.ANY, мы можем обработать любое событие, связанное с нажатием клавиши на клавиатуре. Если же вы хотите обработать только событие, связанное с «отжатием» клавиши, используйте тип KeyEvent.KEY_RELEASED.

Цель события

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

Целью события может быть любой объект, который реализует интерфейс EventTarget. Классы Window, Scene и Node реализуют интерфейс EventTarget.

Механизм обработки событий

Механизм обработки событий состоит из четырех этапов:

  1. выбор цели события (target selection);

  2. построение маршрута события (route construction);

  3. захват события (event capturing);

  4. всплытие события (event bubbling).

1) Выбор цели.

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

· для событий, связанных с клавиатурой, целью является узел, у которого в данный момент есть фокус (что такое фокус – читайте ниже);

· для событий, связанных с мышью, цель это узел, который находится под курсором мыши;

Остальные правила связаны с касаниями и жестами и в данном курсе не рассматриваются (ссылка - https://goo.gl/hihl8G, раздел «Target Selection»).

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

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

Например, у нас есть несколько текстовых полей, в которые требуется ввести некие данные. Одновременно вводить данные в несколько полей мы не можем – значит должно быть что-то, что говорит, какой компонент сейчас активен и в него можно ввести данные с клавиатуры. Указатель, указывающий на поле, в которое мы в данный момент вводим данные, и есть фокус. Фокус могут иметь не только текстовые поля. Его могут иметь, например, и кнопки.

2) Построение маршрута события

Маршрут события строится объектом цели события с помощью метода buildEventDispatchChain(), который вызывается у цели событий. Последовательность объектов, которые должен обойти объект события называется event dispatch chain. Рассмотрим пример:

Цепочка элементов выглядит следующим образом

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

1) Среда Java создает объект события;

2) Среда вычисляет, что целью события является объект класса Circle;

3) Система запрашивает у объекта класса Circle маршрут – как добраться до этого объекта начиная от объекта Stage. В данном случае маршрут будет выглядеть так

Обход маршрута события

Обход маршрута события состоит из двух фаз:

1) Захват события (capturing phase);

2) Всплытие события (Bubbling phase).

Событие обходит каждый узел маршрута дважды: один раз в течение фазы захвата события (сверху вниз) и один раз в течение фазы всплытия события (снизу вверх).

Вы можете зарегистрировать в узле фильтр события (event filter) и обработчик события (event handler), и обрабатывать события определенного типа, когда они будут проходить по маршруту в фазе захвата и в фазе всплытия соответственно.

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

Для регистрации фильтра события используется метод addEventFilter(), а для регистрации обработчика события – метод addEventHandler(). Сами объекты фильтра и обработчика ничем друг от друга не отличаются и оба реализуют интерфейс EventHandler<T extends Event>.

При вызове фильтра\обработчика события, тот объект, у которого был вызван обработчик, называется источником события (event source). То есть, при прохождении по маршруту источник события постоянно меняется.

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

Мы зарегистрировали фильтр событий в узле HBox, и наш узел HBox будет выполнять метод handle() всякий раз, когда объект события, проходя по маршруту, будет проходить через объект HBox сверху вниз, на фазе захвата событий.

Также объект класса Event содержит методы consume() и isConsumed(). Вызов метода consume() означает, что вы указываете событию, что оно «поглощается» текущим обработчиком, и дальнейшая обработка не требуется – событие не идет дальше по цепочке диспетчеров событий. Метод isConsumed() возвращает true, если был вызван метод consume() и false иначе. Модифицируем наш пример следующим образом – напишем обработчик для кнопки 1

Как мы видим, при нажатии на кнопку отрабатывает фильтр в HBox и обработчик в b1. Теперь вернемся в фильтр в HBox и допишем следующее

Мы в фильтре вызвали у события метод consume() и после вызова этого метода, путешествие события по узлам заканчивается – событие «поглощается» и дальше по цепочке не передается.

Как мы видим, фон кнопки не поменялся, т.к. обработчик, который мы добавили в кнопку, не сработал, т.к. после вызова метода consume(), событие не было передано дальше по цепочке. Если вы зарегистрировали в одном узле несколько обработчиков/фильтров и в одном из них вызвали метод consume(), тогда система даст отработать другим обработчикам/фильтрам в этом узле, после чего обработка события останавливается.

Таким образом, расставляя в нужных местах фильтры, обработчики и используя метод consume() мы можем очень тонко настраивать реакцию приложения на различные события. Рассмотрим несколько примеров.

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

Мы можем поступить следующим образом:

1) зарегистрировать обработчик на объекте красного прямоугольника. Внутри обработчика прописать нужное поведение, после чего вызвать метод consume();

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

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

Пример 2. Рассмотрим приложение «Калькулятор», которое мы рассматривали на предыдущей лабораторной работе. Попробуем реализовать функционал для нажатия клавиш цифровой клавиатуры и кнопки «C» (обнулить значение в поле ввода).

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

Так как у нас кнопки расположены внутри менеджера компоновки GridPane, то мы можем создать фильтр событий, который будет обрабатывать событие типа ActionEvent.ACTION. Внутри фильтра мы выясним – было ли нажатие на цифровой блок или на какую-то другую кнопку. Если произошло нажатие на цифровой блок – тогда мы соответствующим образом обработаем нажатие, после чего поглотим событие.

Для реализации кнопки «С» создадим для нее отдельный обработчик события.

1) Отнаследуемся от класса MyButton и создадим класс отдельно для клавиш цифрового блока.

Кнопка цифрового блока будет хранить внутри поле value, которое будет содержать соответствующую цифру цифрового блока.

Далее, пропишем фильтр событий для GridPane

Таким образом, обработку кнопок цифрового блока берет на себя GridPane.

Для кнопки «C» реализуем отдельный обработчик

В итоге получаем нужный нам функционал

Last updated