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

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

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

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

55

и максимум не будет превышать длину и ширину компонента, так работает система рассылки событий.

Резюме

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

Глава 3. За кулисами системы обработки событий

В главе 2 мы рассмотрели основной механизм обработки событий — слушателей. Присоединяя их к компоненту, мы можем узнавать обо всех действиях пользователей, а простые примеры показали прекрасную работу слушателей. Однако иногда возникают более сложные задачи, требующие более изощренного контроля над происходящими событиями, например, если вам понадобится отфильтровать некоторые события или перехватить их раньше, чем они дойдут до слушателей. В этой главе мы познакомимся с «начинкой» системы обработки событий Swing и посмотрим, как она функционирует. Разумеется, эту главу не имеет смысла читать, не ознакомившись с ее основами (см. главу 2).

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

Поток EventDispatchThread и очередь событий EventQueue

Вспомним, как запускается Java-программа: вызывается статический метод main(). При этом виртуальная машина создает новый поток выполнения (который так и называется «main») и передает ему управление. Самое интересное происходит, когда в методе main() создается объект, каким-либо образом связанный с графической системой Java, то есть унаследованный от базового класса java.awt.Component. При этом происходит целая череда событий.

1.Инициализируется динамическая библиотека Java, связывающая графическую подсистему Java с операционной системой, когда в этом возникает необходимость, как правило, с первыми же операциями внутри библиотеки AWT.

2.Создаются необходимые помощники компонентов AWT, отвечающие за их связь с операционной системой.

3.Компоненты выводятся на экран, и операционная система начинает посылать им события (своим «родным» тяжеловесным компонентам). Эти события преобразуются в объекты Java, которые помещаются в очередь событий EventQueue.

4.Очередь событий проверяет наличие потока рассылки событий, и, так как приложение только начинает работу, впервые запускает поток выполнения EventDispatchThread, который связывается с очередью событий EventQueue и начинает рассылать события, хранящиеся в этой очереди событий, по соответствующим компонентам AWT.

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

57

Если с первыми двумя пунктами все более или менее понятно (если вы помните, мы обсуждали помощников компонентов AWT в главе 1), то последние два пункта стоит рассмотреть поподробнее. Итак, что же такое очередь событий? Это экземпляр класса EventQueue из пакета java.awt, и на самом деле класс этот очень прост. Он представляет собой контейнер, работающий по принципу FIFO (First-In, First-Out — первый пришел, первый вышел), то есть по принципу очереди (отсюда и название). В этой очереди графическая система Java хранит все приходящие в программу низкоуровневые события. Происходит это следующим образом: пользователь совершает действие, операционная система сообщает об этом, виртуальная машина Java создает соответствующий событию объект (например, объект класса MouseEvent, если пользователь щелкнул мышью) и добавляет его в конец очереди EventQueue. Класс EventQueue — это одиночка (singleton)1, сколько бы обычных окон, диалоговых окон или компонентов приложение не создавало, очередь событий всегда присутствует в единственном экземпляре. Единственное, что можно сделать, — это заменить существующую очередь событий своей собственной. В том, что очередь событий всегда хранится только в одном экземпляре и реализована в виде очереди, имеется немалый смысл. В очередь событий может поступать громадное количество разнообразных событий, и все из них в той или иной степени изменяют состояние компонента. Если бы события, пришедшие позже, обрабатывались перед событиями, пришедшими раньше, компонент мог бы прийти в противоречивое состояние. Использование дисциплины FIFO позволяет этого избежать. То же самое относится и к наличию только одной очереди: будь их несколько, неизвестно, по каким правилам следовало бы распределять события по очередям и выбирать их оттуда.

Далее вступает в действие специальный поток выполнения EventDispatchThread. Он циклически «подкачивает» (pump) события из очереди событий EventQueue (это делает метод pumpEvents()), и, если события в очереди имеются, он извлекает их и рассылает по соответствующим компонентам AWT (это выполняет метод dispatchEvent() класса EventQueue). Так как поток EventDispatchThread один, очередь событий одна, и события хранятся в порядке их поступления, обеспечивается последовательная обработка событий в соответствии с очередностью их поступления, то есть события обрабатываются одно за другим.

Роль потока EventDispatchThread в любой графической программе на Java трудно переоценить, это настоящий «властелин» этих программ. Именно в этом потоке происходит вся работа, и он никогда не останавливается. Мы отмечали, что графическая программа в промежутке между событиями простаивает, это так лишь с нашей стороны, с другой стороны всегда работает поток EventDispatchThread, вызывающий наш код только при наступлении событий (в графической программе наш код весьма пассивен). Стоит ему остановиться, как программа тут же прекратит свою работу. В этом предложении ответ на часто возникающий вопрос, почему программа, использующая графический интерфейс, не заканчивается после выполнения метода main(), как это делают обычные (консольные) программы. Вот маленький пример:

public class ExitTest {

public static void main(String[] args) { new javax.swing.JFrame().pack();

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

58

ГЛАВА 3

}

}

Если вы запустите эту программу, то увидите, что она и не подумает закончить работу после того, как создаст окно JFrame, установит для него оптимальный размер методом pack(), и метод main() завершится. В спецификации виртуальной машины Java сказано, что программа завершает свою работу, когда в ней не останется работающих потоков выполнения (не являющихся демонами). В графической программе такой поток выполнения остается — это как раз поток распределения событий EventDispatchThread. Прямого доступа к нему нет, так что выход из программы приходится осуществлять «грубой силой»: вызывая метод System.exit()2.

Более того, поток EventDispatchThread рассылает не только события, возникающие

врезультате действий пользователя, в его обязанности входит рассылка событий даже такого экзотического типа, как PaintEvent и InvocationEvent. Вы не сможете обработать

всвоей программе события этого типа, они используются внутри графической подсистемы: событие PaintEvent означает необходимость перерисовать компонент, целиком или частично, а событие InvocationEvent позволяет вмешаться в «плавное течение» потока EventDispatchThread и произвести некоторые дополнительные действия, ненадолго

приостановив его.

Чуть позже мы вернемся к роли потока выполнения EventDispatchThread в графических программах, а сейчас давайте все-таки посмотрим, как события, происходящие в результате действий пользователя, добираются до компонентов Swing. Надо сказать, что низкоуровневые события, возникающие в операционной системе, рассылаются только тяжеловесным компонентам AWT, потому что легковесных компонентов (к которым относятся и компоненты Swing) операционная система не видит. «Прекрасно, — говорите вы, саркастически улыбаясь, — мы договорились до того, что легковесные компоненты Swing не получают уведомления о событиях. Что же, слушатели работают по мановению волшебной палочки?» На самом деле легковесные компоненты узнают о событиях, только не напрямую, а через один скрытый механизм. Сейчас мы с ним познакомимся.

Доставка событий методам processXXXEvent()

Итак, когда поток выполнения EventDispatchThread обнаруживает, что в очереди событий появилось новое событие, он извлекает его и проверяет тип этого события, чтобы определить, что делать с ним дальше. Если извлеченное из очереди событие относится к графическому компоненту (это может быть событие, относящееся

кпрорисовке, или более интересное нам событие, возникшее в результате действия пользователя), то поток EventDispatchThread вызывает метод dispatchEvent() очереди событий EventQueue. Этот метод не делает ничего экстраординарного, а просто узнает,

ккакому компоненту относится событие (то есть определяет источник события — об этом сообщает операционная система) и вызывает метод dispatchEvent() этого компо-

нента.

Все компоненты AWT наследуют метод dispatchEvent() от своего базового класса java. awt.Component, но ни один из них не в состоянии вмешаться в процесс первоначальной обработки событий, потому что метод этот является неизменным (final) и переопределить его невозможно. Сделано это неспроста. Именно в методе dispatchEvent() происходит самая важная внутренняя работа по распределению событий, в том числе координация действий с помощниками компонентов, преобразование событий PaintEvent в вызо-

2 Правда в последних версиях JDK есть и другой способ закончить графическую программу: необходимо удалить из системы все окна их методом dispose(). Если активных окон не останется, поток рассылки завершит свою работу.

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

59

вы методов paint() и update()3, а также целая череда действий, вмешиваться в которые нет смысла, потому что они относятся к внутренней реализации библиотеки и постоянно меняются без всякого предупреждения. Для нас гораздо важнее то, что, в конце концов, метод dispatchEvent() передает событие (если это было событие в ответ на действие пользователя) методу processEvent().

Метод processEvent() — это следующий этап на пути следования событий к слушателям. Ничего особенного в нем не происходит, его основная функция — передать событие согласно его типу одному из методов processXXXEvent(), где «XXX» — название события, например Focus или Key. Именно методы processXXXEvent() распределяют приходящие события по слушателям. Например, метод processKeyEvent() определяет, что именно за событие от клавиатуры пришло, и вызывает соответствующий метод слушателя KeyListener (если конечно, такие слушатели были зарегистрированы в компоненте). После этого мы наконец-то получаем уведомление о событии в свою программу (посредством слушателя) и выполняем необходимые действия. Так заканчивается путь события, начинавшийся в очереди событий. В целом все выглядит так, как показано на рис. 3.1.

 

Слушатели

 

processXXXEvent()

 

processEvent()

 

Component.dispatchEvent()

 

Поток рассылки событий

 

EventDispatchThread

Действия

Очередь событий

пользователя

EventQueue

Рис. 3.1. Путь события к слушателям

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

3 Благодаря непрерывной работе потока EventDispatchThread и событиям PaintEvent графический интерфейс программы способен вовремя перерисовываться, если какая-либо часть его была закрыта другим окном, а затем снова представлена глазам пользователя. Операционная система сообщает Java-приложению о необходимости перерисовки части окна, виртуальная машина создает соответствующее этому сообщению объект-событие PaintEvent, а поток рассылки событий мгновенно, сразу после обнаружения в очереди событий EventQueue нового события PaintEvent, передает это событие в принадлежащий ему компонент (как правило, перерисовка выполняется для главного окна приложения), где данное событие преобразуется в вызов метода paint(). Именно в методе paint() производится прорисовка любого компонента. Программисту-клиенту не нужно знать всех подробностей движения события PaintEvent: довольно знания того, что код, прорисовывающий компонент, необходимо поместить в метод paint().

60

ГЛАВА 3

вцепочку обработки событий еще на уровне метода dispatchEvent()4 и, прежде чем продолжить обработку события обычным образом, «ворошит» все содержащиеся

внем легковесные компоненты, проверяя, не принадлежит ли пришедшее событие им. Если это так, то обработка события тяжеловесным контейнером прекращается, а событие передается в метод dispatchEvent() соответствующего легковесного компонента. Эти действия встроены в базовый класс контейнеров java.awt.Container,

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

Мы упомянули здесь методы processXXXEvent() не для «красного словца», они могут быть полезными и в обычных программах. Чаще всего эти методы используют там, где необходимо выполнить некоторое действие перед поступлением события к слушателю или вообще запретить распространение события. Для этого необходимо переопределить метод processXXXEvent(), соответствующий нужному нам событию. Рассмотрим простой пример, в котором мы попытаемся вмешаться в цепочку обработки событий от мыши:

//PreProcessMouse.java

//Перехват событий от мыши до их поступления к слушателям import javax.swing.*;

import java.awt.event.*;

public class PreProcessMouse extends JFrame { PreProcessMouse() {

super("PreProcessMouse");

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

//добавим слушателя событий от мыши addMouseListener(new MouseL());

//выводим окно на экран

setSize(200, 200); setVisible(true);

}

// перехват событий от мыши

public void processMouseEvent(MouseEvent e) { if ( e.getClickCount() == 1 ) {

// один щелчок не пропускаем к слушателям return;

}

4 Несмотря на то, что метод dispatchEvent() является неизменным (final), вмешаться в его работу контейнер все-таки может, потому что метод dispatchEvent() просто вызывает метод dispatchEventImpl(), в котором и выполняется вся работа. Метод dispatchEventImpl() обладает видимостью в пределах пакета, так что все классы из пакета java.awt могут привнести в него что-то новое.

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

61

// иначе вызываем метод базового класса else super.processMouseEvent(e);

}

// в этом слушателе будем следить за щелчками мыши class MouseL extends MouseAdapter {

@Override

public void mouseClicked(MouseEvent e) { System.out.println(

"ClickCount: " + e.getClickCount());

}

}

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

new Runnable() {

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

}

}

В примере мы создаем небольшое окно JFrame, при его закрытии приложение будет заканчивать свою работу. К этому окну (которое является обыкновенным компонентом, унаследованным от базового класса Component), мы присоединяем слушателя событий от мыши MouseL, который унаследован от адаптера MouseAdapter и следит за щелчками мыши в окне (о них сообщается в метод слушателя mouseClicked()). Однако слушатель этот — не единственное заинтересованное в щелчках мыши «лицо»: мы переопределили метод processMouseEvent() нашего окна JFrame. Как нам теперь известно, именно в этом методе происходит рассылка событий от мыши слушателям, в нем мы также отслеживаем щелчки мыши еще до поступления события к слушателям. Посмотрите, что происходит: в методе processMouseEvent() мы выясняем, сколько раз пользователь щелкнул мышью (не важно, правая эта кнопка или левая), и если количество щелчков равно единице, заканчиваем работу метода, что равносильно игнорированию всех присоединенных слушателей (рассылка событий происходит в методе базового класса, и если мы его не вызываем, то слушатели ничего не узнают). В противном случае, если количество щелчков больше единицы, мы вызываем метод базового класса, в обязанности которого входит рассылка события слушателям.

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

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

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

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

62

ГЛАВА 3

в недоступных для нас механизмах класса Component и Container, и в итоге событие попадает в наши руки уже после того, как система определила, какому компоненту оно принадлежит. То есть фактически фильтровать события мы можем только для того компонента, которому они принадлежат, а смысла в этом немного: чаще всего фильтрация событий имеет смысл для контейнеров, содержащих другие компоненты. Фильтруя или особым образом обрабатывая события для контейнера, к примеру, отключая все щелчки правой кнопки мыши, мы, как правило, хотим, чтобы подобная фильтрация работала и для всех содержащихся в контейнере компонентов. Но реализовать такое поведение, переопределяя методы processXXXEvent(), нельзя: события обрабатываются для каждого компонента отдельно. Создатели Swing учли ситуацию и добавили в контейнеры высшего уровня особый компонент, прозрачную панель; она позволяет фильтровать и предварительно обрабатывать события сразу для всего пользовательского интерфейса программы. Прозрачную панель мы рассмотрим в главе 6, там будет упомянут и похожий по смыслу компонент JXLayer.

Напоследок стоит упомянуть о том, что события от клавиатуры (KeyEvent) заслуживают особого внимания. По многим причинам они не вписываются в общие схемы: среди основных причин — необходимость поддержки механизмов передачи фокуса ввода и клавиатурных сокращений. Фокус ввода определяет, какие компоненты получают события от клавиатуры первыми, а также то, в какой последовательности происходит обработка этих событий. На самом деле, операционная система не видит легковесных компонентов, так что с ее точки зрения все события от клавиатуры происходят только в контейнере высшего уровне, в котором однако может находиться множество легковесных компонентов. Система передачи фокуса ввода должна четко определять, какой компонент получит событие от клавиатуры. Эта система встроена в работу метода dispatchEvent() базового класса Component и благодаря этому всегда имеет шанс перехватить любое необходимое для передачи фокуса ввода событие от клавиатуры. Клавиатурные сокращения — один из основополагающих инструментов Swing, поддерживаются благодаря особой реализации метода processKeyEvent() базового класса библиотеки JComponent. Поэтому переопределение метода processKeyEvent() для специальной обработки или фильтрации событий от клавиатуры нужно производить с осторожностью

ивсегда вызывать метод базового класса super.processKeyEvent(), а также осознавать, что некоторые нажатия клавиш до этого метода могут так и не добраться. Мы будем подробно обсуждать систему передачи фокуса ввода и клавиатурные сокращения в главе 5,

итам же узнаем, какие дополнительные способы специальной обработки событий от клавиатуры у нас есть.

Маскирование и поглощение событий

При окончательной «шлифовке» системы событий ее создатели заметили, что методы компонентов processEvent() и processXXXEvent(), служащие для рассылки событий, могут становится «узким местом» графических программ: в них вполне способна «затеряться» некоторая часть процессорного времени. Исследование показывает, что в компоненты приходят уведомления абсолютно обо всех происходящих с ними низкоуровневых событиях, будь то события от мыши, клавиатуры, изменения в размерах контейнера или что-либо еще, причем приходят такие уведомления всегда, независимо от того, заинтересован компонент в этих событиях или нет, и в легковесные, и в тяжеловесные компоненты. В итоге получалось, что и для сложных компонентов, таких как таблицы, по-настоящему нуждающихся в информации о большей части событий, и для простых вспомогательных компонентов, подобных панелям, которые по большому счету нуждаются только в своевременной перерисовке, система присылала полную информацию обо всех происходящих событиях. И это не приводило к блестящей производительности.

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

63

Решением стало маскирование событий (event masking). С каждым компонентом графической системы, способным получать уведомления о событиях, теперь ассоциирована специальная маска в виде длинного (long) целого, которая и определяет, какие события компонент получает. По умолчанию отключено получение всех событий, кроме уведомлений о необходимости перерисовки. Но, как только вы (или другой компонент) присоединяете к компоненту слушателя события определенного типа, например, слушателя событий от мыши MouseListener, компонент тут же включает в свою маску признак обработки данного события (от мыши), так что система обработки событий начинает присылать ему все связанные с этой маской события. Принцип прост: нет слушателей — нет и событий, как только слушатели появляются — автоматически появляются события.

Маскирование скорее относится к внутренним механизмам системы обработки событий, и чаще всего вам достаточно того, что при присоединении слушателей маска компонента автоматически обновляется. Однако вы можете и вручную управлять маскированием, быстро включая (или отключая) доставку интересующих вас событий. Это менее гибкий способ управления событиями, чем, к примеру, методы processXXXEvent(), но иногда он позволяет добиться желаемого гораздо быстрее. Одним из самых распространенных вариантов «ручного» маскирования можно назвать как раз переопределение методов processXXXEvent(): если вы не присоедините к компоненту слушателей, а попытаетесь сразу же получить информацию о событиях из методов processXXXEvent(), у вас ничего не выйдет. Вспомните, по умолчанию события в маску не включаются. Это распространенная ошибка, а решение ее просто: надо просто включить в маску нужное вам событие, после чего метод processXXXEvent() заработает «как по маслу».

Добавить событие в маску компонента или удалить его оттуда позволяет пара методов enableEvents() и disableEvents(). Правда, вызвать их напрямую не так то просто: методы эти описаны как защищенные (protected), то есть они доступны только подклассам компонента. В качестве параметров им требуется передать набор объединенных логическим «ИЛИ» констант из класса AWTEvent, которые и определят, какие низкоуровневые события вы включаете или отключаете. Рассмотрим небольшой пример:

//MaskingEvents.java

//Маскирование событий import java.awt.*; import javax.swing.*;

public class MaskingEvents extends JFrame { public MaskingEvents() {

super("MaskingEvents");

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

//отключим события от окна disableEvents(AWTEvent.WINDOW_EVENT_MASK);

//добавим особую кнопку

JPanel contents = new JPanel(); contents.add(new CustomButton("Привет!")); setContentPane(contents);

// выведем окно на экран setSize(400, 300); setVisible(true);

64

ГЛАВА 3

 

}

 

// особая кнопка

 

class CustomButton extends JButton {

 

public CustomButton(String label) {

 

super(label);

 

// отключаем события с клавиатуры и от мыши

 

disableEvents(AWTEvent.KEY_EVENT_MASK);

 

disableEvents(AWTEvent.MOUSE_EVENT_MASK);

 

}

 

}

 

public static void main(String[] args) {

SwingUtilities.invokeLater( new Runnable() {

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

}

}

Здесь мы создаем небольшое окно с рамкой JFrame и сразу же указываем ему, что при закрытии окна нужно будет завершить работу приложения. В нашем примере все меняется: мы пытаемся властно приказать окну (методом disableEvents(), он нам доступен, потому что класс в примере унаследован от окна JFrame, а оно, в свою очередь, от базового класса Component) исключить из маски события от окна. Однако после запуска примера вы убедитесь, что окно не обратило внимания на нашу маску и закрывается так же, как и обычное окно. В первом издании книги пример работал, и окно не закрывалось, однако это было сочтено нарушением безопасности пользователей, особенно при работе из апплетов, и маскирование оконных событий теперь отключено. Это первая особенность системы маскирования событий.

Пример также демонстрирует, как управлять маскированием событий от клавиатуры; мы отключаем их для особой кнопки, унаследованной от обычной кнопки JButton библиотеки Swing. Запустив программу с примером, вы увидите, что созданную кнопку легко нажать мышью, но невозможно активизировать с клавиатуры (с помощью клавиши Enter или пробела в зависимости от используемых внешнего вида и поведения). Все работает как обычно, слушатели и UI-представители готовы к обработке событий, но они просто не доходят до них: маскирование выполняется на самых нижних уровнях системы обработки событий, и если событие не включено в маску компонента, обработать его оказывается невозможно.

Однако тут же маскирование демонстрирует еще одну особенность, постойте-ка, что мы только что сказали? «Кнопку легко нажать мышью» - но глянув на код примера, совершенно очевидно, что мы отключили события от мыши. Это вторая особенность маскирования событий — событие маскируется только в том случае, если оно не включено в маску событий и если для событий данного типа нет слушателей. Однако наша кнопка самаприсоединила слушателя, потому что желает знать, когда на нее нажмут, и придуманная нами маска снова оказывается не у дел.

ВНИМАНИЕ

Маскирование событий не так уж и очевидно, как кажется, — оно не работает, если событие ожидает хотя бы один слушатель, неважно, кем и когда он добавлен, и не работает для оконных событий.