Модель событий |
55 |
и максимум не будет превышать длину и ширину компонента, так работает система рассылки событий.
Резюме
Итак, события могут возникать в любом компоненте Swing; для того чтобы узнать о них, надо создать соответствующего слушателя событий и зарегистрировать его в компоненте. В большинстве случаев обработка событий не заходит дальше этой простой цепочки действий, и в этой главе мы рассмотрели все ее детали, так что создание и присоединение слушателей не является для нас проблемой. Однако при работе с системой событий возникают и более сложные ситуации, как правило, это связано с появлением при обработке событий различных потоков выполнения. Более сложную часть обработки событий мы рассмотрим в следующей главе.
Глава 3. За кулисами системы обработки событий
В главе 2 мы рассмотрели основной механизм обработки событий — слушателей. Присоединяя их к компоненту, мы можем узнавать обо всех действиях пользователей, а простые примеры показали прекрасную работу слушателей. Однако иногда возникают более сложные задачи, требующие более изощренного контроля над происходящими событиями, например, если вам понадобится отфильтровать некоторые события или перехватить их раньше, чем они дойдут до слушателей. В этой главе мы познакомимся с «начинкой» системы обработки событий Swing и посмотрим, как она функционирует. Разумеется, эту главу не имеет смысла читать, не ознакомившись с ее основами (см. главу 2).
Программы, написанные на Java, выполняются виртуальной машиной Java (Java Virtual Machine, JVM), которая обеспечивает независимость от конкретной платформы. Виртуальная машина использует ресурсы операционной системы для решения своих задач, это утверждение верно и для событий пользовательского интерфейса. Низкоуровневые события (движения мыши, нажатия клавиш, закрытие окон) возникают именно в операционной системе, виртуальная машина перехватывает их и перенаправляет компонентам пользовательского интерфейса Java-приложения. Посмотрим теперь, как низкоуровневые события попадают к компонентам Java (к которым относятся и компоненты Swing).
Поток EventDispatchThread и очередь событий EventQueue
Вспомним, как запускается Java-программа: вызывается статический метод main(). При этом виртуальная машина создает новый поток выполнения (который так и называется «main») и передает ему управление. Самое интересное происходит, когда в методе main() создается объект, каким-либо образом связанный с графической системой Java, то есть унаследованный от базового класса java.awt.Component. При этом происходит целая череда событий.
1.Инициализируется динамическая библиотека Java, связывающая графическую подсистему Java с операционной системой, когда в этом возникает необходимость, как правило, с первыми же операциями внутри библиотеки AWT.
2.Создаются необходимые помощники компонентов AWT, отвечающие за их связь с операционной системой.
3.Компоненты выводятся на экран, и операционная система начинает посылать им события (своим «родным» тяжеловесным компонентам). Эти события преобразуются в объекты Java, которые помещаются в очередь событий EventQueue.
4.Очередь событий проверяет наличие потока рассылки событий, и, так как приложение только начинает работу, впервые запускает поток выполнения EventDispatchThread, который связывается с очередью событий EventQueue и начинает рассылать события, хранящиеся в этой очереди событий, по соответствующим компонентам AWT.
За кулисами системы обработки событий |
57 |
Если с первыми двумя пунктами все более или менее понятно (если вы помните, мы обсуждали помощников компонентов AWT в главе 1), то последние два пункта стоит рассмотреть поподробнее. Итак, что же такое очередь событий? Это экземпляр класса EventQueue из пакета java.awt, и на самом деле класс этот очень прост. Он представляет собой контейнер, работающий по принципу FIFO (First-In, First-Out — первый пришел, первый вышел), то есть по принципу очереди (отсюда и название). В этой очереди графическая система Java хранит все приходящие в программу низкоуровневые события. Происходит это следующим образом: пользователь совершает действие, операционная система сообщает об этом, виртуальная машина Java создает соответствующий событию объект (например, объект класса MouseEvent, если пользователь щелкнул мышью) и добавляет его в конец очереди EventQueue. Класс EventQueue — это одиночка (singleton)1, сколько бы обычных окон, диалоговых окон или компонентов приложение не создавало, очередь событий всегда присутствует в единственном экземпляре. Единственное, что можно сделать, — это заменить существующую очередь событий своей собственной. В том, что очередь событий всегда хранится только в одном экземпляре и реализована в виде очереди, имеется немалый смысл. В очередь событий может поступать громадное количество разнообразных событий, и все из них в той или иной степени изменяют состояние компонента. Если бы события, пришедшие позже, обрабатывались перед событиями, пришедшими раньше, компонент мог бы прийти в противоречивое состояние. Использование дисциплины FIFO позволяет этого избежать. То же самое относится и к наличию только одной очереди: будь их несколько, неизвестно, по каким правилам следовало бы распределять события по очередям и выбирать их оттуда.
Далее вступает в действие специальный поток выполнения EventDispatchThread. Он циклически «подкачивает» (pump) события из очереди событий EventQueue (это делает метод pumpEvents()), и, если события в очереди имеются, он извлекает их и рассылает по соответствующим компонентам AWT (это выполняет метод dispatchEvent() класса EventQueue). Так как поток EventDispatchThread один, очередь событий одна, и события хранятся в порядке их поступления, обеспечивается последовательная обработка событий в соответствии с очередностью их поступления, то есть события обрабатываются одно за другим.
Роль потока EventDispatchThread в любой графической программе на Java трудно переоценить, это настоящий «властелин» этих программ. Именно в этом потоке происходит вся работа, и он никогда не останавливается. Мы отмечали, что графическая программа в промежутке между событиями простаивает, это так лишь с нашей стороны, с другой стороны всегда работает поток EventDispatchThread, вызывающий наш код только при наступлении событий (в графической программе наш код весьма пассивен). Стоит ему остановиться, как программа тут же прекратит свою работу. В этом предложении ответ на часто возникающий вопрос, почему программа, использующая графический интерфейс, не заканчивается после выполнения метода main(), как это делают обычные (консольные) программы. Вот маленький пример:
public class ExitTest {
public static void main(String[] args) { new javax.swing.JFrame().pack();
1 Одиночкой класс EventQueue является с точки зрения библиотеки AWT, которая одновременно задействует только один его экземпляр. Однако вы можете создать сколь угодно много объектов этого класса, и заменить используемый AWT объект EventQueue своим собственным (это может быть и объект специальным образом унаследованного класса с новой функциональностью). Тем не менее, используется только один объект.
58 |
ГЛАВА 3 |
}
}
Если вы запустите эту программу, то увидите, что она и не подумает закончить работу после того, как создаст окно JFrame, установит для него оптимальный размер методом pack(), и метод main() завершится. В спецификации виртуальной машины Java сказано, что программа завершает свою работу, когда в ней не останется работающих потоков выполнения (не являющихся демонами). В графической программе такой поток выполнения остается — это как раз поток распределения событий EventDispatchThread. Прямого доступа к нему нет, так что выход из программы приходится осуществлять «грубой силой»: вызывая метод System.exit()2.
Более того, поток EventDispatchThread рассылает не только события, возникающие
врезультате действий пользователя, в его обязанности входит рассылка событий даже такого экзотического типа, как PaintEvent и InvocationEvent. Вы не сможете обработать
всвоей программе события этого типа, они используются внутри графической подсистемы: событие PaintEvent означает необходимость перерисовать компонент, целиком или частично, а событие InvocationEvent позволяет вмешаться в «плавное течение» потока EventDispatchThread и произвести некоторые дополнительные действия, ненадолго
приостановив его.
Чуть позже мы вернемся к роли потока выполнения EventDispatchThread в графических программах, а сейчас давайте все-таки посмотрим, как события, происходящие в результате действий пользователя, добираются до компонентов Swing. Надо сказать, что низкоуровневые события, возникающие в операционной системе, рассылаются только тяжеловесным компонентам AWT, потому что легковесных компонентов (к которым относятся и компоненты Swing) операционная система не видит. «Прекрасно, — говорите вы, саркастически улыбаясь, — мы договорились до того, что легковесные компоненты Swing не получают уведомления о событиях. Что же, слушатели работают по мановению волшебной палочки?» На самом деле легковесные компоненты узнают о событиях, только не напрямую, а через один скрытый механизм. Сейчас мы с ним познакомимся.
Доставка событий методам processXXXEvent()
Итак, когда поток выполнения EventDispatchThread обнаруживает, что в очереди событий появилось новое событие, он извлекает его и проверяет тип этого события, чтобы определить, что делать с ним дальше. Если извлеченное из очереди событие относится к графическому компоненту (это может быть событие, относящееся
кпрорисовке, или более интересное нам событие, возникшее в результате действия пользователя), то поток EventDispatchThread вызывает метод dispatchEvent() очереди событий EventQueue. Этот метод не делает ничего экстраординарного, а просто узнает,
ккакому компоненту относится событие (то есть определяет источник события — об этом сообщает операционная система) и вызывает метод dispatchEvent() этого компо-
нента.
Все компоненты AWT наследуют метод dispatchEvent() от своего базового класса java. awt.Component, но ни один из них не в состоянии вмешаться в процесс первоначальной обработки событий, потому что метод этот является неизменным (final) и переопределить его невозможно. Сделано это неспроста. Именно в методе dispatchEvent() происходит самая важная внутренняя работа по распределению событий, в том числе координация действий с помощниками компонентов, преобразование событий PaintEvent в вызо-
2 Правда в последних версиях JDK есть и другой способ закончить графическую программу: необходимо удалить из системы все окна их методом dispose(). Если активных окон не останется, поток рассылки завершит свою работу.
За кулисами системы обработки событий |
59 |
вы методов paint() и update()3, а также целая череда действий, вмешиваться в которые нет смысла, потому что они относятся к внутренней реализации библиотеки и постоянно меняются без всякого предупреждения. Для нас гораздо важнее то, что, в конце концов, метод dispatchEvent() передает событие (если это было событие в ответ на действие пользователя) методу processEvent().
Метод processEvent() — это следующий этап на пути следования событий к слушателям. Ничего особенного в нем не происходит, его основная функция — передать событие согласно его типу одному из методов processXXXEvent(), где «XXX» — название события, например Focus или Key. Именно методы processXXXEvent() распределяют приходящие события по слушателям. Например, метод processKeyEvent() определяет, что именно за событие от клавиатуры пришло, и вызывает соответствующий метод слушателя KeyListener (если конечно, такие слушатели были зарегистрированы в компоненте). После этого мы наконец-то получаем уведомление о событии в свою программу (посредством слушателя) и выполняем необходимые действия. Так заканчивается путь события, начинавшийся в очереди событий. В целом все выглядит так, как показано на рис. 3.1.
|
Слушатели |
|
processXXXEvent() |
|
processEvent() |
|
Component.dispatchEvent() |
|
Поток рассылки событий |
|
EventDispatchThread |
Действия |
Очередь событий |
пользователя |
EventQueue |
Рис. 3.1. Путь события к слушателям
Вся описанная цепочка действий относится к тяжеловесным компонентам AWT, которые «видны» операционной системе и которым она посылает низкоуровневые события. Как же легковесные компоненты получают уведомления о событиях? Оказывается, что происходит это с помощью тяжеловесного контейнера (обычно окна или апплета), в котором расположены легковесные компоненты. Он вмешивается
3 Благодаря непрерывной работе потока EventDispatchThread и событиям PaintEvent графический интерфейс программы способен вовремя перерисовываться, если какая-либо часть его была закрыта другим окном, а затем снова представлена глазам пользователя. Операционная система сообщает Java-приложению о необходимости перерисовки части окна, виртуальная машина создает соответствующее этому сообщению объект-событие PaintEvent, а поток рассылки событий мгновенно, сразу после обнаружения в очереди событий EventQueue нового события PaintEvent, передает это событие в принадлежащий ему компонент (как правило, перерисовка выполняется для главного окна приложения), где данное событие преобразуется в вызов метода paint(). Именно в методе paint() производится прорисовка любого компонента. Программисту-клиенту не нужно знать всех подробностей движения события PaintEvent: довольно знания того, что код, прорисовывающий компонент, необходимо поместить в метод paint().