17. События в JavaFX

Обработка событий играет важную роль, поскольку большинство элементов управления 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