За кулисами системы обработки событий |
65 |
Маскирование редко требуется в обычных приложениях, однако его можно применить в специальных целях, таких как автоматизированное тестирование интерфейса или проверка работоспособности приложения в условиях отказа одного или нескольких видов событий. Впрочем, стоит помнить, что, так же как и в случае с методами processXXXEvent(), маскирование событий работает только для одного компонента, что может быть неудобно. В таких случаях нам снова может пригодиться прозрачная панель или инструмент JXLayer, которые мы будем обсуждать в главе 6.
При обработке низкоуровневых событий вы при необходимости можете поглощать (consume) их. Это означает буквально следующее: вы при обработке события в слушателе или методе processXXXEvent() приходите к выводу, что обработку данного события можно завершить прямо сейчас, то есть передавать его дальше для какой-либо дополнительной обработки больше не следует. Осуществить это и позволяет механизм поглощения событий. Пример привести нетрудно: в слушателе событий от клавиатуры, присоединенном к текстовому полю, появляется событие нажатия некоторой клавиши. Ваша программа особым образом обрабатывает его, но не хочет, чтобы этот символ был передан текстовому полю обычным порядком, что приведет к появлению символа на экране. В своем слушателе вы поглощаете событие, и оно не попадает к текстовому полю.
Поглощение события выполняет метод consume(), определенный в базовом классе всех низкоуровневых событий AWTEvent. Поглощение работает только для низкоуровневых событий (имеются в виду события от клавиатуры и мыши), да и то со многими оговорками: после вызова метода consume() событие не доберется до самого компонента, если он каким-то образом переопределил методы processXXXEvent(), и операционная система не сможет обработать его обычным образом (если этот компонент тяжеловесен и обрабатывает события). Однако до всех зарегистрированных в компоненте слушателей событие все равно доберется, и будут ли они обрабатывать уже «поглощенное» событие или нет (было ли событие поглощено, позволяет узнать метод isConsumed()), зависит только от их «доброй воли». Таким образом, для библиотеки Swing (компоненты которой легковесны и не обладают помощниками) поглощение не слишком полезно, если они следят за событиями с помощью слушателей. Небольшой пример проиллюстрирует механизм поглощения:
//ConsumingEvents.java
//Поглощение событий import java.awt.*; import java.awt.event.*; import javax.swing.*;
public class ConsumingEvents extends JFrame { public ConsumingEvents() {
super("ConsumingEvents");
// при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);
// слушатель, поглощающий печатание символов
KeyListener kl = new KeyAdapter() { @Override
public void keyTyped(KeyEvent e) { e.consume();
}
66 |
ГЛАВА 3 |
};
//добавляем текстовые поля setLayout(new FlowLayout());
JTextField swingField = new JTextField(10); swingField.addKeyListener(kl); add(swingField);
TextField awtField = new TextField(10); add(awtField); awtField.addKeyListener(kl);
//кнопка
JButton button = new JButton("Жмите!"); add(button);
// слушатель пытается поглотить события от мыши button.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) { e.consume();
}
});
// выводим окно на экран setSize(300, 200); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new ConsumingEvents(); } });
}
}
Мы создаем небольшое окно, в котором у нас будет два текстовых поля — по одному из библиотек Swing и AWT и кнопка. Для начала мы пробуем проверить, будут ли исправно поглощаться события нажатия клавиш на клавиатуре, если мы вызовем для них consume(). Запустив программу, вы увидите, что будут, а это значит текстовые компоненты Swing получают события от клавиатуры не от слушателей, а на более ранних этапах
ипоглощение работает. Так как поле AWT по сути является системным компонентом, оно также не получит поглощенные нами события. С кнопкой все сложнее — несмотря на наши попытки «съесть» событие, она все равно реагирует на мышь, а значит, получает события через слушатели, и мы не можем так просто это ей запретить.
Компоненты Swing являются легковесными, то есть полностью написанными на Java,
ичасто они обрабатывают события с помощью обычных слушателей, поэтому поглощенные события до них в основном все равно дойдут. Если вы хотите гарантированно перехватить некоторое событие и «не пустить» его к легковесному компоненту, используйте подходящий метод processXXXEvent(), возможности окон Swing, которые мы разберем
в главе 6, или наследование от класса компонента. Поглощение вам в этом не поможет.
За кулисами системы обработки событий |
67 |
Работа с очередью событий
Очередь EventQueue является настоящим ядром системы обработки событий: все события, так или иначе возникающие в вашей программе, проходят через нее, включая даже такие экзотические события, как уведомления о перерисовке PaintEvent. Тем примечательнее тот факт, что вы имеете доступ к очереди событий, используемой в вашей программе, и более того, можете унаследовать от класса EventQueue, переопределить некоторые его методы, и использовать в программе свой вариант очереди событий, получая над системой обработки событий практически неограниченную власть.
Получить используемую в данный момент очередь событий позволяет метод getSystemEventQueue() класса Toolkit, а получить объект Toolkit можно методом getToolkit(), который имеется в каждом унаследованном от класса Component компоненте (кстати, класс Toolkit — это абстрактная фабрика, используемая для создания основных частей AWT). Полученный экземпляр очереди событий позволяет проделать многое: вы сможете узнать, какие события находятся в данный момент в очереди событий, вытащить их оттуда, поместить в очередь новые события (которые могут быть созданы вами вручную, а всем компонентам будет «казаться», что события эти возникли в результате действий пользователя). Помещение в очередь сгенерированных вами событий — прекрасный способ автоматизированного тестирования вашего интерфейса или демонстрации некоторых его возможностей. Приложение при этом будет вести себя точно так же, как если бы с ним работал самый настоящий пользователь. Рассмотрим небольшой пример:
//UsingEventQueue.java
//Использование очереди событий import java.awt.*;
import java.awt.event.*; import javax.swing.*;
public class UsingEventQueue extends JFrame { public UsingEventQueue() {
super("UsingEventQueue");
//выход при закрытии окна setDefaultCloseOperation(EXIT_ON_CLOSE);
//кнопка и ее слушатель
JButton button = new JButton("Генерировать событие"); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { // генерируем событие закрытия окна
getToolkit().getSystemEventQueue().postEvent( new WindowEvent(UsingEventQueue.this, WindowEvent.WINDOW_CLOSING));
}
}); // добавим кнопку в панель содержимого
setLayout(new FlowLayout());
68 ГЛАВА 3
add(button);
// выведем окно на экран setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingEventQueue(); } });
}
}
В примере мы создаем небольшое окно, при закрытии которого программа будет завершать свою работу. В панель содержимого окна (для нее мы устанавливаем последовательное расположение компонентов FlowLayout) добавляется кнопка JButton с присоединенным к ней слушателем. При нажатии кнопки мы создаем событие WindowEvent типа WINDOW_CLOSING, именно такое событие генерируется виртуальной машиной, когда пользователь пытается закрыть окно. Созданное событие (помимо типа ему нужно указать источник, то есть окно, которое закрывается пользователем) мы помещаем в очередь событий, используя для этого метод postEvent(). Запустив программу с примером, вы увидите, что при нажатии кнопки приложение заканчивает свою работу, точно так же, как если бы мы нажали кнопку закрытия окна. Система обработки событий «играет почестному» — те события, которые виртуальная машина генерирует в ответ на действия пользователя, вы можете с тем же успехом генерировать самостоятельно — результат будет точно таким же.
Заменить стандартную очередь событий собственным вариантом очереди позволяет метод push() класса EventQueue. Ему нужно передать экземпляр вашей очереди событий, унаследованной от класса EventQueue. Наибольший интерес при наследовании представляет метод dispatchEvent(), который, как мы знаем, вызывается при распределении событий всех типов. Переопределив данный метод, вы получите исчерпывающую информацию о том, как, когда и какие события распределяются в вашей программе. Это может быть весьма ценно при отладке и диагностике вашей программы или при реализации особого поведения ваших компонентов5.
Влияние на программы потока EventDispatchThread
Теперь, когда мы в подробностях обсудили детали системы обработки событий, настала пора вернуться к потоку распределения событий EventDispatchThread и той роли, которую он играет в любой графической программе. Мы уже знаем, что этот поток распределяет по компонентам события, находящиеся в очереди событий EventQueue, и запускается той самой очередью, когда в нее попадают первые события графической системы. Таким образом, если ваша программа использует графические компоненты, она автоматически оказывается в многозадачном окружении, и вам придется принимать во внимание вопросы совместного использования ресурсов и их синхронизации. «Почему же программа оказывается в многозадачном окружении?» — спросите вы.
5 Есть чуть менее сложный способ узнать обо всех распределяемых в вашей программе низкоуровневых событиях — использовать особый слушатель AWTEventListener. Вы присоединяете его к системе обработки событий с помощью класса Toolkit, указывая при этом, о событиях каких типов вы хотите знать. После присоединения слушателя AWTEventListener события всех указанных вами типов будут перед распределением попадать этому слушателю.
За кулисами системы обработки событий |
69 |
«Ведь в ней остается только один поток, поток рассылки событий? С кем он может конфликтовать?». Во-первых, даже в простейших программах, вроде тех, что мы уже разбирали, кроме потока рассылки событий всегда есть еще один поток выполнения с названием main. В нем выполняется, как нетрудно догадаться, метод main(), и он вполне может конфликтовать с потоком рассылки событий. Ну а, во-вторых, важнейшим свойством любого пользовательского интерфейса является его отзывчивость. Если программа замирает хотя бы на несколько секунд, не показывая при этом признаков жизни, это приводит пользователя в самую настоящую ярость. Если не использовать для выполнения длинных сложных задач отдельные потоки (так чтобы не блокировать рассылку событий и работу программы), обеспечить отзывчивость интерфейса будет невозможно.
Прежде всего выясним, когда именно запускается поток рассылки событий. Если брать в расчет библиотеку AWT, основу Swing, то там считается, что поток рассылки событий запускается, как только какой-либо компонент переходит в реализованное (realized) состояние. Реализованное состояние компонента означает, что он становится видимым для операционной системы, начинает получать от нее сообщения о событиях и занимает некоторое место на экране или в памяти экрана (даже если он все еще невидим). При получении событий очередь событий запускает поток рассылки. Легковесные компоненты (к которым относятся и компоненты Swing), а их операционная система не видит, переходят в реализованное состояние одновременно с переходом в реализованное состояние тяжеловесных контейнеров (окон или апплетов), в которых они содержатся. В свою очередь, тяжеловесные контейнеры переходят в реализованное состояние после вызова одного из следующих методов:
pack() — служит для придания окну оптимального размера;
setVisible(true) или show() — выводит окно на экран.
Если вы вызвали один из этих методов, можете быть уверены в том, что поток рассылки событий уже начал свою работу, и вы находитесь в многозадачном окружении. До вызова одного из этих методов графические компоненты AWT являются просто объектами, и вы можете работать с ними из любого потока.
Вопросы синхронизации с потоком рассылки событий становятся особенно важны при работе с компонентами библиотеки Swing, потому что последние практически полностью лишены каких бы то ни было средств для работы в многозадачном окружении. Это сделано не случайно: при разработке библиотеки рассматривалось множество альтернатив, и было выяснено, что наделение компонентов механизмами поддержки многозадачного окружения приведет не только к их усложнению, но и к значительному падению скорости их работы. Поэтому с компонентами Swing работать имеет смысл только из потока рассылки событий, то есть только из слушателей или методов, служащих для обработки событий. Но тут возникает резонный вопрос: «А как же обеспечить отзывчивость интерфейса? Ведь длительные вычисления в потоке рассылки событий блокируют остальной интерфейс программы. И неужели придется забыть обо всех удобствах, которые дает параллельное выполнение нескольких потоков?».
На самом деле все не так плохо. Во-первых, у вас есть несколько методов, которые можно вызывать из любого потока выполнения, не опасаясь соревнования с потоком рассылки событий. Они обладают встроенной синхронизацией и всю работу с компонентами все равно вызовут из потока рассылки событий. Вот эти методы:
repaint() — служит для перерисовки компонента;
revalidate(), validate(), invalidate() — позволяют заново расположить компоненты в контейнере и удостовериться в правильности их размеров.