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

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

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

Модель событий

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) {

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) обозначает начало компонента,