50 |
ГЛАВА 2 |
listenerList.remove(l);
}
// прорисовывает кнопку
public void paintComponent(Graphics g) {
//зальем зеленым цветом g.setColor(Color.green);
g.fillRect(0, 0, getWidth(), getHeight());
//рамка
g.setColor(Color.black);
g.draw3DRect(0, 0, getWidth(), getHeight(), true);
}
// оповещает слушателей о событии protected void fireButtonPressed() {
for (ButtonPressListener l: listenerList) {
l.buttonPressed(event);
}
}
// внутренний класс, следит за нажатиями мыши class PressL extends MouseAdapter {
// нажатие мыши в области кнопки
public void mousePressed(MouseEvent e) { // оповестим слушателей
fireButtonPressed();
}
}
}
Мы создаем очень простой компонент SimpleButton: это прямоугольник с рамкой, который тем не менее обладает собственным событием ButtonPressEvent. Компонент унаследован нами от базового класса JComponent библиотеки Swing, так что все действия, связанные с его прорисовкой, мы поместили в метод paintComponent() (подробнее о базовом компоненте Swing и реализованной в нем системе прорисовки мы узнаем в главе 3). В конструкторе происходит настройка основных механизмов компонента: мы присоединяем к нему слушателя событий от мыши, реализованного во внутреннем классе PressL, а также задаем размер нашего компонента методом setPreferredSize() (по умолчанию размеры компонента считаются нулевыми). В слушателе PressL мы будем отслеживать нажатия кнопок мыши, для этого нам понадобится метод mousePressed(). Как только пользователь щелкнет кнопкой мыши в области, принадлежащей нашему компоненту, будет вызван метод fireButtonPressed(), обязанностью которого является оповещение слушателей о событии. Кстати, название вида fireXXX() (или fireXXXEvent()) является неофициальным стандартом для методов, «запускающих» высокоуровневые события, хотя такие методы и не описаны в спецификации JavaBeans. Мы еще не раз встретим методы с такими названиями при работе с событиями различных компонентов Swing и их моделями.
Слушатели ButtonPressListener будут храниться в списке ArrayList, адаптированном только под хранение объектов ButtonPressListener. Благодаря мощи стандартного
Модель событий |
51 |
списка ArrayList методы для присоединения и отсоединения слушателей совсем просты: им нужно лишь использовать соответствующие возможности списка. Также просто и оповещение слушателей о событиях: для каждого элемента списка мы вызываем метод buttonPressed(), передавая ему в качестве параметра объект-событие. Объект-событие у нас один на компонент, и именно он передается всем слушателям: на самом деле, зачем создавать для каждого слушателя новое событие, если в нем не хранится ничего, кроме ссылки на источник события, то есть на сам компонент. Мы не включили в компонент поддержку многозадачности: если регистрировать слушателей будут несколько потоков одновременно, у нашего компонента могут возникнуть проблемы (для эффективной работы список ArrayList рассчитан на работу только с одним потоком в каждый момент времени). Но исправить это легко: просто объявите методы для присоединения и отсоединения слушателей как synchronized. Аналогичное действие проделайте и с методом fireButtonPressed() (он тоже работает со списком). Есть и еще один способ включить для компонента поддержку многозадачного окружения: с помощью класса java.util.Collections и статического метода synchronizedList(). Последний метод вернет вам версию списка ArrayList со встроенной поддержкой многозадачности. Впрочем, как мы узнаем из следующей главы, работать с компонентами Swing из нескольких потоков без специальных усилий нельзя, так что смысла в добавлении слушателей из нескольких потоков как правило никакого нет.
Ну а теперь давайте проверим работу нашего нового компонента и посмотрим, будет ли обрабатываться принадлежащее ему событие. Вот простой пример:
//SimpleButtonTest.java
//Обработка события нового компонента import javax.swing.*;
import com.porty.swing.*; import com.porty.swing.event.*;
import java.awt.*;
public class SimpleButtonTest extends JFrame { public SimpleButtonTest() {
super("SimpleButtonTest"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем кнопку и присоединим слушателей
SimpleButton button = new SimpleButton();
//анонимный класс button.addButtonPressListener(
new ButtonPressListener() {
public void buttonPressed(ButtonPressEvent e) { System.out.println("1!");
}
});
//внутренний класс
button.addButtonPressListener(new ButtonL());
52 |
ГЛАВА 2 |
//добавим кнопку в окно
JPanel contents = new JPanel(); contents.add(button); setContentPane(contents);
//выведем окно на экран setSize(400, 300); setVisible(true);
}
class ButtonL implements ButtonPressListener { public void buttonPressed(ButtonPressEvent e) {
System.out.println("2!");
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new SimpleButtonTest(); } });
}
}
В примере мы создаем небольшое окно, в панели содержимого которого разместится наш новый компонент, простая «кнопка» SimpleButton (в качестве панели содержимого используется отдельная панель JPanel). К нашей кнопке мы присоединяем два слушателя ButtonPressListener: один слушатель описан прямо на месте, в виде анонимного класса, а второй реализован во внутреннем классе. После добавления кнопки в окно последнее выводится на экран. Запустив программу с примером, вы увидите (посмотрев на консольный вывод), как слушатели оповещаются о событии.
Подобная цепочка действий повторяется для любого нового события JavaBeans: вы описываете класс события и интерфейс его слушателя, следуя хорошо известным правилам, и добавляете в компонент пару методов для регистрации слушателей и их отсоединения. Для хранения слушателей используется подходящий список, все хранящиеся в нем слушатели оповещаются о возникновении события.
Обратите также внимание, что хранение слушателей в простых коллекциях данных, таких как список ArrayList, накладывает на слушателя определенные ограничения. В самом деле, слушатель вызывается в момент перечисления элементов списка, и удалить себя или другого слушателя в этот момент не сможет, так как список на это не рассчитан. Модификацию списка слушателей лучше проводить не из кода слушателя.
Список EventListenerList
В пакете javax.swing.event, который предназначен для хранения событий и слушателей компонентов библиотеки Swing, есть интересный инструмент — специализированный список EventListenerList. Этот список позволяет хранить в одном месте произвольное количество слушателей любого типа, лишь бы они были унаследованы от «отличительного знака» слушателей — интерфейса EventListener. Для того чтобы можно было легко различать слушателей разных типов, при добавлении в список каждому слушателю требуется сопоставить его «тип», который представлен объектом Class. Таким образом
Модель событий |
53 |
список EventListenerList состоит из пар вида «объект Class — слушатель EventListener». Для добавления в список новой такой пары служит метод add(). Если бы мы в только что разобранном нами примере использовали для хранения слушателей вместо ArrayList список EventListenerList, то для присоединения слушателей нам понадобилось бы написать следующий код:
listenerList.add(ButtonPressListener.class, l);
Получить слушателей определенного типа позволяет метод getListeners(), которому необходимо передать объект Class, определяющий тип слушателей. Данный метод возвращает массив слушателей нужного нам типа. Для получения слушателей ButtonPressListener пригодился бы такой код:
EventListener[] listeners = listenerList.getListeners(ButtonPressListener.class);
С полученным массивом слушателей легко работать: для каждого элемента массива (все элементы массива имеют базовый тип EventListener) вы можете смело провести преобразование к указанному вами в методе getListeners() типу слушателя (у нас это ButtonPressListener) и вызвать определенные в интерфейсе слушателя методы, которые и сообщат слушателям о том, что в компоненте произошли некоторые события. Помимо метода getListeners() можно также использовать метод getListenersList(), возвращающий все содержащиеся в списке пары Class — EventListener в виде массива. С массивом, полученным из метода getListenersList(), работать сложнее: приходится самостоятельно проверять, принадлежит ли слушатель нужному нам типу и учитывать тот факт, что каждый слушатель хранится вместе с объектом Class. Метод getListeners() без сомнения проще и удобнее.
Практически все компоненты библиотеки Swing, обладающие собственными событиями, и стандартные модели с поддержкой слушателей используют для хранения слушателей список EventListenerList. Если в вашем компоненте или модели имеется событие только одного типа (и соответственно, слушатель одного типа), проще задействовать обычный список, вроде ArrayList. Но если событий и слушателей несколько, преимущества EventListenerList выходят на первый план: достаточно единственного экземпляра такого списка для хранения всех регистрируемых в вашем компоненте или модели слушателей; не нужно понапрасну расходовать память на несколько списков и выполнять излишнюю работу. Требуется лишь правильно указывать, слушателя какого типа вы собираетесь добавить в список, удалить из него (для этого предназначен метод remove()) или получить.
События от мыши и волшебный метод contains()
Хотя мы только начали знакомиться с событиями, сразу же стоит упомянуть одну особенность работы с событиями от мыши. Дело в том, что они весьма зависимы от области на экране, которую занимает интересующий вас компонент: только в том случае, когда курсор мыши находится в области компонента, мы можем сказать, что происходят перемещения курсора, щелчки или перетаскивания. Заведует же определением местонахождения той или иной точки экрана в области компонента метод contains(), определенный в базовом классе всех компонентов Java Component.
По умолчанию нам, как правило, ничего не приходится делать — область, которую выделяет компоненту контейнер (панель, окно или апплет) и является той зоной, в которой происходят все события от мыши. Это понятное и прозрачное поведение. Но бывает, что нам нужно модифицировать это поведение, особенно если компонент не прямоугольной формы или имеет какие-то «окна», сквозь которые события от мыши проникают к компонентам, лежащим ниже в контейнере. Ситуация в Swing не такая уж
54 |
ГЛАВА 2 |
редкая, и мы увидим примеры в главе 4. В качестве быстрого и понятного примера можно посмотреть на такую кнопку:
//ContainsTest.java
//Изменение поведения мыши и метод contains() import javax.swing.*;
import java.awt.*;
public class ContainsTest extends JFrame { public ContainsTest() { super("ContainsTest");
//при закрытии окна - выход setDefaultCloseOperation(EXIT_ON_CLOSE);
//добавим кнопку и переопределим метод
JButton button = new JButton("Невидима") { @Override
public boolean contains(int x, int y) {
//не содержим ни одной точки
return false;
}
}; // настроим панель содержимого и выведем окно на экран
setLayout(new FlowLayout()); add(button);
setSize(300, 200); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new ContainsTest(); } });
}
}
В этом чрезвычайно простом примере мы создаем маленькое окно и размещаем в нем кнопку. Кнопка ничем не отличается от своих собратьев за исключением метода contains() — этот метод для переданных ему координат точки должен сообщить, принадлежит ему точка или нет. Мы говорим, что нам чужда любая точка из нашей области, и запустив пример, вы увидите, что происходит. Кнопка не нажимается мышью, хотя она доступна и включена, но свободно нажимается с клавиатуры.
Из мелочей следует помнить, что необходимо переопределять именно метод, которому передаются два целых числа, координаты компонента X и Y, а не второй вариант метода с объектом Point, он системой не вызывается. Ну и координаты исчисляются в системе самого компонента — (0,0) обозначает начало компонента,