Модель событий |
45 |
// рассылаем события по методам
if ( e.getSource() == button1 ) onOK(e);
if ( e.getSource() == button2 ) onCancel(e);
}
}
// обработка события от кнопки "ОК" public void onOK(ActionEvent e) { System.out.println("onOK()");
}
// обработка события от кнопки "Отмена" public void onCancel(ActionEvent e) { System.out.println("onCancel()");
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new ForwardingEvents(); } });
}
}
В данном примере создается окно, в которое помещено две кнопки. К каждой кнопке присоединен слушатель событий Forwarder, следящий за нажатиями кнопок, причем слушатель этот создается только один раз (что без сомнений позволяет экономить память). В самом слушателе проделывается немудреная работа: при возникновении события от кнопки выясняется, в какой именно кнопке произошло это событие, после чего вызывается метод с соответствующим названием. Слушатель Forwarder можно расширить, чтобы он поддерживал гораздо большее число кнопок, и при этом не придется создавать новые классы — достаточно будет лишь определить новые методы. Если в дальнейшем понадобится модифицировать работу приложения, это будет несложно: надо унаследовать новый класс от класса окна и переопределить интересующие нас методы, например onOK().
Диспетчеризация имеет свои преимущества: код получается более компактным и в некотором смысле более привычным для тех программистов, что перешли на Java с других языков программирования, где обработка событий осуществляется именно в методах, а не в отдельных классах. Именно такая иллюзия и создается в результате использования такой техники: мы видим несколько методов, вызываемых при возникновении событий, и реализуем обработку этих событий, переопределяя методы. Однако есть здесь и свои ограничения: то, как события рассылаются по методам, целиком зависит от классов слушателей, подобных Forwarder, и если событие от какого-то компонента не обрабатывается этим классом, вам остается лишь развести руками и писать слушатель самому. Если компонентов в интерфейсе достаточно много, и для каждого из них создается свой метод, обрабатывающий некоторое событие, получится гигантский класс, «битком набитый» методами, а работать с такими классами всегда неудобно; более того, появление таких классов свидетельствует о нарушении основополагающего правила объектно-ориентированного программирования: каждый класс решает собственную небольшую задачу.
46 |
ГЛАВА 2 |
Если вы используете какое-либо визуальное средство создания интерфейса, и в нем для обработки событий требуется диспетчеризация, прекрасно. Задействуйте методы, которые это средство генерирует, и в них обрабатывайте события, по крайней мере, до тех пор, пока код сохраняется более или менее чистым и управляемым. Но если вы пишете код для обработки событий самостоятельно, создавайте для слушателей событий отдельные классы. Это прекрасно структурирует код и избавляет вас от дополнительной скучной работы (написания диспетчера, хранения бесполезных ссылок и создания множества новых методов).
Проблема висячих ссылок
Обработка событий в Swing реализована на очень высоком уровне, в чем вы уже не раз могли убедиться: события обрабатываются просто и так, как вам необходимо. Однако в системе обработки событий есть свойство, которое иногда необходимо учитывать, чтобы не привести программу к катастрофе.
Рассмотрим ситуацию, в которой программа создает компоненты и добавляет их в интерфейс динамически, прямо во время работы, заранее не зная, сколько их будет. Хорошим примером является любое средство визуальной разработки пользовательского интерфейса: во время создания интерфейса в контейнер добавляется
иудаляется множество компонентов, и к каждому из них присоединяются некоторые слушатели (чаще всего это слушатели привязанных свойств компонентов). Пользователь такого средства может экспериментировать с интерфейсом, создавать все новые
иновые обычные и диалоговые окна, добавлять в них новые компоненты. К каждому из таких компонентов визуальное средство добавляет слушателя событий (или даже нескольких слушателей). Затем пользователь может удалить эти компоненты, закрыть текущий контейнер и снова открыть его — все это приведет к удалению компонентов, но ссылки на слушатели в них останутся, потому что визуальное средство не вызвало метод removeXXXListener(), отсоединяющий ранее присоединенных слушателей. Такие
ссылки и называются висячими.
Теперь надо вспомнить, как в Java работает сборщик мусора. Хорошо известно, что созданные объекты в Java не нужно явно удалять: об этом заботится сборщик мусора. Он работает в фоновом режиме параллельно с программой, периодически включается и производит удаление объектов, на которые не осталось явных ссылок. Здесь нас и поджидает сюрприз — все те графические компоненты, которые визуальное средство удалило из контейнера, не удаляются сборщиком мусора, потому что в них еще имеются явные ссылки на слушателей событий, ранее присоединенных визуальным средством. И чем больше будет работать программа, чем интенсивнее пользователь будет создавать интерфейсы, тем меньше останется памяти, и в конце концов все может завершиться аварийным завершением программы с потерей несохраненных данных.
Описанная ситуация возникает не так уж и редко, потому что Swing прекрасно подходит для программ с динамическим пользовательским интерфейсом: нет ничего проще добавления компонентов в контейнер и присоединения к ним слушателей прямо «на ходу». Поэтому, если вы пишете программу с заранее неизвестным количеством компонентов, не забывайте отсоединять от удаляемых компонентов ранее присоединенных слушателей методом removeXXXListener(), особенно если эти слушатели используются многократно и привязаны к каким-либо еще компонентам системы, которые еще активны и не стали мусором. В обычных же программах, вроде тех, что мы рассматривали в этой главе, нет необходимости строго следить за слушателями — количество компонентов ограничено, и все они остаются на месте на время работы программы.
Кстати, висячие ссылки — это проблема не системы обработки событий Swing, а побочный эффект использования шаблона проектирования «наблюдатель». Везде, где он
Модель событий |
47 |
применяется, в том или ином виде возникают висячие ссылки (субъект и наблюдатель чаще всего имеют разное время жизни), и с ними приходится бороться. Тем не менее, дефект это не такой уж серьезный, и достоинства наблюдателя перевешивают его недостатки.
Создание собственных событий
Компоненты библиотеки Swing настолько разнообразны и функциональны, что их возможностей с лихвой хватает для нужд большей части приложений. Поэтому львиную долю времени нам приходится обрабатывать события уже имеющихся компонентов, и мы только что подробно рассмотрели, как это можно делать.
Впрочем, как бы ни были хороши стандартные компоненты библиотеки, рано или поздно возникают ситуации, когда нужные нам возможности они обеспечить не могут. В таком случае придется создать собственный компонент, унаследовав его от какого-ли- бо компонента библиотеки или написав «с нуля» (то есть унаследовав от базового компонента JComponent библиотеки Swing или, если вы хотите создать компонент «с чистого листа», от базового класса Component библиотеки AWT). У вашего нового компонента, если он выполняет не самые простые функции, наверняка будут какие-то события,
ипрограммистам-клиентам компонента (если компонент окажется удачным, возможности для его многократного использования обязательно найдутся) необходимо предоставить способ обработки этих событий. Для компонента, соответствующего архитектуре JavaBeans, это означает наличие интерфейса слушателя, класса события и пары методов для присоединения и удаления слушателей. Чуть раньше мы кратко обсудили систему именования событий JavaBeans и правила, которым подчиняются слушатели и классы событий. Давайте попробуем создать свой компонент и новый тип события. Следование архитектуре JavaBeans к тому же позволит использовать новый компонент и его события в визуальных средствах разработки.
Вкачестве примера рассмотрим простую кнопку: небольшой прямоугольник с рамкой, при нажатии которого происходит некоторое событие. Написание собственно компонента (его процедуры прорисовки) выльется всего в несколько простых строк кода, гораздо интереснее процесс создания события (нажатия кнопки) для этого компонента.
Прежде всего необходимо создать класс события. Как вы помните из описания схемы событий JavaBeans, этот класс должен быть унаследован от класса java.util.EventObject
ииметь название вида XXXEvent, где XXX — название события. Вот что получается для
нашей кнопки:
//com/porty/swing/event/ButtonPressEvent.java
//Событие (нажатие) для кнопки
package com.porty.swing.event;
import java.util.EventObject;
public class ButtonPressEvent extends EventObject { // Конструктор. Требует задать источник события public ButtonPressEvent(Object source) {
super(source);
}
}
48 |
ГЛАВА 2 |
В нашем событии не будет храниться никакой дополнительной информации, так что класс события чрезвычайно прост. Заметьте, что конструктор класса требует указать источник события; как правило, это компонент, в котором событие произошло. Источник события нужно задавать для любого события, унаследованного от класса EventObject, а получить его позволяет метод getSource() того же базового класса. Таким образом, при обработке любого события JavaBeans вы можете быть уверены в том, что источник этого события всегда известен. И, наконец, обратите внимание на то, что класс нашего нового события разместился в пакете com.porty.swing.event, и к нему проще организовать доступ. При создании других событий вам вовсе не обязательно делать их классы такими же простыми: вы можете добавлять в них подходящие поля и методы, которые сделают работу с событием более комфортной.
Далее нам нужно описать интерфейс слушателя нашего события. Данный интерфейс будут реализовывать программисты-клиенты компонента, заинтересованные в нажатиях кнопки. Интерфейс слушателя, следующего стандарту JavaBeans, должен быть унаследован от интерфейса java.util.EventListener. В последнем нет ни одного метода, он служит «отличительным знаком», показывая, что наш интерфейс описывает слушателя событий. Итак:
//com/porty/swing/event/ButtonPressListener.java
//Интерфейс слушателя события нажатия кнопки package com.porty.swing.event;
import java.util.EventListener;
public interface ButtonPressListener extends EventListener {
// данный метод будет вызываться при нажатии кнопки void buttonPressed(ButtonPressEvent e);
}
Винтерфейсе слушателя мы определили всего один метод buttonPressed(), который
ибудет вызываться при нажатии нашей кнопки. В качестве параметра этому методу передается объект события ButtonPressEvent, так что заинтересованный в нажатии кнопки
программист, реализовавший интерфейс слушателя, будет знать подробности о событии. Интерфейс слушателя, так же как и класс события, разместился в пакете com.porty. swing.event.
Теперь нам остается включить поддержку события в класс самого компонента. Для этого в нем нужно определить пару методов для присоединения и отсоединения слушателей ButtonPressListener, эти методы должны следовать схеме именования событий JavaBeans. В нашем случае методы будут именоваться addButtonPressListener() и removeButtonPressListener(). Слушатели, которых программисты регистрируют в данных методах, будут оповещаться о нажатии кнопки. Существует два основных способа регистрации слушателей в компоненте и оповещения их о происходящих событиях.
Регистрация единичного (unicast) слушателя. В классе компонента определяется единственная ссылка на слушателя события, так что узнавать о событии может только один слушатель одновременно. При присоединении нового слушателя старый слушатель, если он был, перестает получать оповещения о событиях, так как ссылка на него теряется.
Регистрация произвольного (multicast) количества слушателей. В компоненте заводится список, в котором и хранятся все присоединяемые к компоненту слушатели.
Модель событий |
49 |
При возникновении события все находящиеся в списке слушатели получают о нем полную информацию.
В подавляющем большинстве ситуаций используется гораздо более гибкий и удобный второй способ с произвольным количеством слушателей, практически все события стандартных компонентов Swing (это же относится и к компонентам AWT), низкоуровневые и высокоуровневые, поддерживают произвольное количество слушателей. Для нас не составит особого труда реализовать список слушателей в нашем компоненте, так что мы тоже выбираем второй способ. Все готово для того, чтобы приступить к созданию самого компонента:
//com/porty/swing/SimpleButton.java
//Пример компонента со своим собственным событием package com.porty.swing;
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*;
import com.porty.swing.event.*;
public class SimpleButton extends JComponent { // список слушателей
private ArrayList<ButtonPressListener>
listenerList = new ArrayList<ButtonPressListener>();
//один объект-событие на все случаи жизни private ButtonPressEvent event =
new ButtonPressEvent(this);
//конструктор - присоединяет к кнопке слушателя
//событий от мыши
public SimpleButton() { addMouseListener(new PressL()); // зададим размеры компонента
setPreferredSize(new Dimension(100, 100));
}
//присоединяет слушателя нажатия кнопки public void addButtonPressListener(
ButtonPressListener l) { listenerList.add(l);
}
//отсоединяет слушателя нажатия кнопки public void removeButtonPressListener(
ButtonPressListener l) {