Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Портянкин И. Swing

.pdf
Скачиваний:
140
Добавлен:
07.10.2020
Размер:
4.63 Mб
Скачать

За кулисами системы обработки событий

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() — позволяют заново расположить компоненты в контейнере и удостовериться в правильности их размеров.

70

ГЛАВА 3

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

Ну и, наконец, есть самый гибкий способ изменить компонент из другого потока. Если ваша задача, выполняющаяся в отдельном потоке, так или иначе приводит к необходимости изменить что-либо в компоненте, и в этом вам не могут помочь безопасные вызовы, остается только одно. Во избежание неприятностей с потоком рассылки событий и неожиданного тупика все действия с компонентами все равно нужно выполнять из потока рассылки событий. И у вас есть возможность выполнить некоторое действие в потоке рассылки событий. Для этого предназначены методы invokeLater() и invokeAndWait() класса EventQueue. Данным методам нужно передать ссылку на интерфейс Runnable, метод run() этого интерфейса будет выполнен потоком рассылки событий, как только он доберется до него. (Переданная в метод invokeLater() или invokeAndWait() ссылка на интерфейс Runnable «оборачивается» в событие специального типа InvocationEvent, обработка которого сводится к вызову метода run().) В итоге вы действуете следующим образом: выполняете в отдельном потоке сложные долгие вычисления, получаете некоторые результаты, а действия, которые необходимо провести после этого с графическими компонентами, выполняете в потоке рассылки событий с помощью метода invokeLater() или invokeAndWait(). Рассмотрим небольшой пример:

//InvokeLater.java

//Метод invokeLater() и работа с потоком рассылки событий import java.awt.*;

import java.awt.event.*; import javax.swing.*;

public class InvokeLater extends JFrame { public InvokeLater() {

super("InvokeLater");

//при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);

//добавим кнопку со слушателем

button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) { // запустим отдельный поток

new ComplexJobThread().start(); button.setText("Подождите...");

}

}); // настроим панель содержимого и выведем окно на экран

setLayout(new FlowLayout()); add(new JTextField(20)); add(button);

setSize(300, 200);

За кулисами системы обработки событий

71

setVisible(true);

}

private JButton button;

// поток, выполняющий "сложную работу" class ComplexJobThread extends Thread {

public void run() { try {

//изобразим задержку sleep(3000);

//работа закончена, нужно изменить интерфейс

EventQueue.invokeLater(new Runnable() { public void run() {

button.setText("Работа завершена");

}

});

}catch (Exception ex) {

ex.printStackTrace();

}

}

}

public static void main(String[] args) { SwingUtilities.invokeLater(

new Runnable() {

public void run() { new InvokeLater(); } });

}

}

Мы создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки будет вызван слушатель ActionListener, и мы предполагаем, что работа, которая предстоит слушателю, довольно сложна и займет приличное время, поэтому выполнять ее нужно в отдельном потоке (на самом деле, выполнение долгих вычислений в слушателе, который был вызван потоком рассылки событий, приведет к блокированию остальных событий, ждущих в очереди рассылки тем же потоком, и в итоге интерфейс программы станет неотзывчивым). Отдельный поток, выполняющий долгие сложные вычисления, реализован во внутреннем классе ComplexJobThread, унаследованном от базового класса всех потоков Thread. Проблема состоит в том, что по окончании вычислений нам нужно сменить надпись на кнопке, а мы к этому моменту будем находиться в отдельном потоке. Менять надпись из потока, отличного от потока рассылки событий, не стоит: это может привести к неопределенности данных в компонентах и ошибкам, и даже к тупикам

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

иприйти в нестабильное состояние.

Здесь нам и пригодится статический метод invokeLater() класса EventQueue. Он позволяет выполнить некоторый фрагмент кода из потока рассылки событий. В качестве

72 ГЛАВА 3

параметра данному методу нужно передать ссылку на объект, который реализует интерфейс Runnable: метод run(), определенный в этом интерфейсе, и будет выполнен потоком рассылки событий. Так мы и поступаем в примере: код, меняющий надпись на кнопке, будет выполнен из потока рассылки событий, так что можно не опасаться возникновения конфликтов. Запустив программу с примером и нажав кнопку, вы увидите, что во время вычислений пользовательский интерфейс доступен (вы сможете набрать что-ли- бо в текстовом поле или нажать кнопку еще раз). Сразу по завершении вычислений вы узнаете об этом по изменению надписи не кнопке, и никаких конфликтов при этом не возникнет.

Помимо метода invokeLater() в вашем распоряжении также имеется метод invokeAndWait(). Он аналогичным образом позволяет выполнить фрагмент кода из потока рассылки событий, но в отличие от invokeLater(), делает это синхронно: приостанавливая работу потока, из которого вы его вызвали, до тех пор, пока поток рассылки событий не выполнит ваш код. С другой стороны, invokeLater() работает асинхронно: он немедленно возвращает вам управление, так что вы можете продолжать работу, зная, что рано или поздно ваш фрагмент кода будет выполнен потоком рассылки событий. В большинстве ситуаций предпочтительнее метод invokeLater(), а метод invokeAndWait() стоит использовать только там, где немедленное выполнение потоком рассылки фрагмента вашего кода обязательно для дальнейших действий. Работая с методом invokeAndWait(), следует быть внимательнее: поток, из которого вы его вызвали, будет ожидать, когда поток рассылки событий выполнит переданные ему фрагмент кода, а в этом фрагменте кода могут быть обращения к ресурсам, принадлежащим первому потоку, тому самому, что находится в ожидании. В итоге возникнет взаимная блокировка, и программа окажется в тупике. Метод invokeLater() позволяет всего этого избежать.

Практически все компоненты Swing не обладают встроенной синхронизацией, но благодаря описанным двум методам класса EventQueue вы всегда сможете выполнить некоторые действия с компонентами из потока рассылки событий. Правда, есть несколько исключений, к примеру, текстовые компоненты Swing, такие как многострочные поля JTextArea или редакторы JEditorPane, позволяют изменять свой текст из другого потока (и это очень удобно, особенно при загрузке больших текстов). Таких исключений немного, и если компонент может работать в многозадачном окружении, вы увидите упоминание об этом в его интерактивной документации и в этой книге.

Принципы работы потока рассылки событий и рассмотренный только что пример плавно подводят нас к «золотому правилу» Swing: пишите слушатели короткими и быстрыми. Здесь все просто: рассылка событий и вызов соответствующих слушателей, в которых они обрабатываются, происходят из одного потока выполнения, потока рассылки событий EventDispatchThread. Как только какой-либо слушатель начинает производить долгие вычисления, все остальные события «застревают» в очереди событий, и интерфейс программы перестает отзываться на любые действия пользователя, а работать с такой неповоротливой программой — удовольствие ниже среднего.

СОВЕТ

Если в слушателе или методе обработки событий выполняются достаточно длинные вычисления, выносите их в отдельный поток. Манипулировать графическими компонентами из другого потока выполнения вы всегда сможете с помощью методов класса EventQueue или класса SwingUtilities из пакета javax.swing. Последний позволяет избежать импорта ненужных пакетов AWT и именно его рекомендуется применять в Swing .

За кулисами системы обработки событий

73

Следуя этому нехитрому совету, с помощью Swing вы всегда будете писать эффектные и скоростные пользовательские интерфейсы. Зачастую незнание этого правила приводит к написанию чудовищно неповоротливых программ, а вину за это их создатели сваливают на Swing и Java, которые, тем не менее, позволяют разрабатывать интерфейсы, работающие не менее быстро, чем интерфейсы приложений «родной» операционной системы, что неоднократно доказано на практике даже очень большими приложениями.

Отзывчивость программы и SwingWorker

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

В библиотеке Swing имеется класс, способный облегчить нашу участь, и скрыть некоторые детали процесса за своими понятными методами. Он называется SwingWorker, и задача его состоит в том, что элегантно разделить исполнение основной задачи, обновление промежуточных результатов в пользовательском интерфейсе (конечно же, из потока рассылки событий) и позволить выполнить какие-либо действия с интерфейсом после окончания вычислений.

Давайте попробуем переделать предыдущий пример с «долгими» вычислениями

спомощью SwingWorker. Вот что у нас получится:

//UsingSwingWorker.java

//Класс SwingWorker для отзывчивости интерфейса import javax.swing.*;

import java.awt.*; import java.awt.event.*; import java.util.List;

public class UsingSwingWorker extends JFrame { private JButton button;

public UsingSwingWorker() { super("UsingSwingWorker");

//при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);

//добавим кнопку со слушателем

button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) { // запустим отдельную долгую работу

new ComplexJob().execute(); button.setText("Подождите...");

}

});

74

ГЛАВА 3

// настроим панель содержимого и выведем окно на экран setLayout(new FlowLayout());

add(new JTextField(20)); add(button); setSize(300, 200); setVisible(true);

}

// класс, выполняющий "сложную работу"

class ComplexJob extends SwingWorker<String,String> { // здесь выполняется работа, это отдельный поток! public String doInBackground() throws Exception {

Thread.sleep(2000);

// публикуем промежуточные результаты publish("Половина работы закончена..."); Thread.sleep(2000);

return "";

}

//обработка промежуточных результатов

//это поток рассылки событий!

protected void process(List<String> chunks) { button.setText(chunks.get(0));

}

// окончание работы - и вновь это поток рассылки public void done() {

button.setText("Работа завершена");

}

}

public static void main(String[] args) { SwingUtilities.invokeLater(

new Runnable() {

public void run() { new UsingSwingWorker(); } });

}

}

Мы вновь создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки нас ожидает тяжелая, долгая, неповоротливая задача, и во имя наших пользователей мы обязаны выполнить ее вне потока рассылки событий. Теперь нам помогает класс SwingWorker, специально предназначенный для этих задач.

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