110 |
ГЛАВА 5 |
SwingUtilities.invokeLater(new Runnable() { |
|
public void run() |
{ |
newButton = new |
JButton("Новичок"); |
button.getParent().add(newButton);
}
});
Thread.sleep(2000);
// revalidate() может быть вызван из любого потока newButton.revalidate();
}
}
В этом примере мы снова используем Swing, а значит должны работать со всеми компонентами из потока рассылки событий. Как и в примере с AWT, создается окно с двумя кнопками. Однако, Swing тут же доказывает свое удобство, автоматически проверяя корректность кнопки при смене ее надписи методом setText(). Затем мы добавляем в панель с кнопками еще одну кнопку, что не приведет к автоматической проверке. Чтобы ее провести, мы вызываем revalidate() для самой кнопки. Это найдет корень валидации (корневую панель нашего окна) и заново расположит все компоненты в ней. Для какого компонента вызывается revalidate(), не так уж и важно.
Сам метод revalidate() может быть безопасно вызван из любого потока — он автоматически заботится о том, чтобы дальнейшие действия происходили в потоке рассылке событий.
На деле пример не так прост, как кажется. Если вам повезет, вы сможете увидеть как новая кнопка появляется в окне сразу же, еще до вызова revalidate(). Секрет тут прост: в таких случаях мы успеваем добавить ее еще до того, как выполнилась задача проверки корректности, запущенная методом setText(), и она «прихватывает» с собой и новый компонент в панели.
Интересно, что удобство Swing, благодаря которому компоненты сами вызывают revalidate() при смене своих размеров, является чуть ли не главной причиной того, что даже до вывода приложения на экран с компонентами можно работать только из потока рассылки событий. На самом деле, revalidate() помещает задание в очередь событий (это не так, только если компонент пока не добавлен в контейнер), а это автоматически запускает поток рассылки. Насколько это оправдывает себя, вопрос спорный. Иногда возможность заранее подготовить части сложного интерфейса в фоновом режиме бывает очень кстати, но Swing это сделать не позволяет.
Проверка корректности в Swing: резюме
Соединяя вместе правила работы классического метода validate() и более изощренного revalidate(), мы получаем следующие выводы:
revalidate() эффективнее и пытается объединить похожие события в пакет. Однако вы не можете рассчитывать на мгновенный эффект, и должны быть уверены в том, что среди контейнеров, в которых находится ваш компонент, найдется корень валидации (isValidateRoot() возвращает true). В противном случае проверки не произойдет.
validate() проводит проверку немедленно, но только среди потомков контейнера, для которого вы его вызываете. Контейнер нужно выбрать правильно, чтобы привести к корректному виду все нужные вам компоненты. Для Swing этот метод можно вызывать только из потока рассылки событий.
При разработке собственных сложных компонентов, не меняющих свои размеры (к примеру, легковесных плавающих диалогов), вам может пригодится метод
Внутренние винтики Swing |
111 |
isValidateRoot(). Правильный выбор корня валидации увеличит производительность, не «пуская» валидацию за пределы нужного контейнера. Это важно в Swing, если помнить что практически любое изменение моделей или свойств стандартных компонентов автоматически запускает revalidate().
Оба этих метода сработают, только если компонент, который вы хотите привести к корректному виду, уже находится на экране и виден. Действительно, для невидимых компонентов нужные размеры будут заданы в момент их вывода на экран.
Клавиатурные сокращения
При создании Swing одной из приоритетных целей было создание продуманной и универсальной системы клавиатурных сокращений (keyboard bindings, или keyboard shortcuts). В большинстве современных оконных систем клавиатурные сокращения используются весьма широко, и не зря: возможность совершить какое-либо частое или сложное действие путем нажатия двух–трех клавиш очень удобно и ценится пользователями, потому что всегда намного быстрее работы мышью.
Поддержка клавиатурных сокращений обеспечивается базовым классом библиотеки JComponent. Начинается все с метода processKeyEvent(), основной целью которого является сортировка событий от клавиатуры и рассылка их соответствующим методам зарегистрированных слушателей событий от клавиатуры (мы обсуждали его в главе 2). Класс JComponent бесцеремонно вмешивается в этот процесс, переопределяя данный метод и заменяя стандартный механизм своей цепочкой обработки событий от клавиатуры. Но прежде чем выяснять, как это происходит, познакомимся с классом KeyStroke и картами входных событий и команд, принимающими в этом непосредственное участие.
Класс KeyStroke
Класс KeyStroke инкапсулирует клавиатурное сокращение, позволяя указать, в случае какого события от клавиатуры оно возникает. Ничего особого этот класс не делает, он просто хранит код символа (или сам символ), клавиша которого нажимается для активизации клавиатурного сокращения, и набор модификаторов, уточняющих условие срабатывания клавиатурного сокращения, например, удержание управляющей клавиши Ctrl или Alt. Также можно указать, какой именно тип нажатия клавиши приведет к срабатыванию сокращения: это может быть простое нажатие клавиши (используемое чаще всего), печать символа (пригодное только для клавиш, отвечающих за печатные символы, а также для клавиш Enter и Esc) или отпускание клавиши, когда сокращение срабатывает только при отпускании заданного сочетания клавиш (которые предварительно были нажаты и могли удерживаться).
Создать экземпляр класса KeyStroke напрямую у вас не получится, потому что все его конструкторы объявлены закрытыми (private). Создатели этого класса решили таким образом встроить в него возможность кэширования создаваемых объектов KeyStroke, предвидя ситуацию, когда в приложении может быть задействовано значительное количество одинаковых клавиатурных сокращений (например, при использовании в приложении нескольких текстовых компонентов, у каждого из которых имеются одинаковые сокращения для частых действий, таких как выделение текста). Создать объект класса KeyStroke позволяют определенные в нем перегруженные версии статического метода getKeyStroke()1. Когда вы вызываете этот метод для создания объекта KeyStroke, он прежде всего проверяет, не содержится ли объект с такой же информацией в кэше, и при положительном ответе возвращает вам кэшированный объект. Так как объекты KeyStroke всего
1 Одновременно с выполнением своих основных обязанностей класс KeyStroke еще и функционирует как фабрика по созданию своих экземпляров.
112 |
ГЛАВА 5 |
лишь хранят информацию о клавиатурных сокращениях и сами не выполняют никаких действий, использование одного и того же объекта в разных местах программы безопасно и, кроме того, позволяет реально экономить память и время на инициализацию.
Карты входных событий и команд
Клавиатурные сокращения хранятся в специальных картах (таблицах, map), позволяющих строить иерархию клавиатурных сокращений от компонентов-роди- телей к компонентам-потомкам и проще манипулировать ими. Такие карты дают возможность легко изменить реакцию компонента на любые воздействия с клавиатуры и легко настраиваются.
Первая такая карта, хранящаяся в классе JComponent, называется картой входных событий (input map). Это экземпляр класса InputMap; он представляет собой отображение некоторых входных событий, которые воспринимает компонент, на объекты, отвечающие за действие компонента в случае возникновения этих событий. В идеале в такой карте должны храниться все входные события, воспринимаемые компонентом, но пока Swing использует ее лишь для хранения клавиатурных сокращений, поддерживаемых компонентом. Все клавиатурные сокращения, которые должен обрабатывать компонент, необходимо поместить в эту карту, для этого в классе InputMap определен метод put(KeyStroke, Object). В качестве второго параметра метода чаще всего указывают строку, идентифицирующую действие, которое должно вызывать помещаемое в карту клавиатурное сокращение (впрочем, вторым параметром может быть любой объект). Новое клавиатурное сокращение обычно добавляют в уже имеющуюся в компоненте карту входных событий, которую можно получить методом getInputMap(). Однако реакцию компонента на действия пользователя с клавиатуры можно и полностью изменить, установив свою карту методом setInputMap(). Именно так поступают UI-представители при настройке компонента, это позволяет им «одним махом» придать компоненту поведение, свойственное ему на той платформе, которую они представляют.
На самом деле у компонентов не одна карта входных событий, а целых три. Каждая их них хранит клавиатурные сокращения, поддерживаемые компонентом в одном из трех состояний (табл. 5.1).
Таблица 5.1. Состояния компонента при обработке клавиатурных сокращений
Состояние |
Описание |
WHEN_FOCUSED |
Компонент обладает фокусом ввода, |
|
то есть все события прежде всего идут |
|
к нему, а затем уже к родительским ком- |
|
понентам |
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT |
Фокусом ввода обладает один из потом- |
|
ков компонента. Ни один из потомков |
|
событие от клавиатуры не обработал, |
|
и оно предлагается самому компоненту |
WHEN_IN_FOCUSED_WINDOW |
Событиепришлоккакому-токомпонен- |
|
ту, находящемуся в том же окне, что |
|
и наш компонент. Тот компонент и его |
|
предки событие не обработали, и оно |
|
по очереди предлагается всем находя- |
|
щимся в окне компонентам Swing |
Получить карту входных событий для определенного состояния позволяет все тот же метод getInputMap(), точнее, его перегруженная версия, которой необходимо
Внутренние винтики Swing |
113 |
передать константу, идентифицирующую это состояние. Вызов этого метода без параметров возвращает карту для состояния WHEN_FOCUSED. Таким образом вы можете зарегистрировать клавиатурные сокращения для любого состояния вашего компонента. Чаще других используются состояния WHEN_FOCUSED (когда сокращение срабатывает лишь в том случае, если компонент обладает фокусом ввода: к примеру, выделение текста в текстовом поле имеет смысл только в этом состоянии) и WHEN_ IN_FOCUSED_WINDOW (когда для инициирования какого-либо глобального действия в программе сокращение должно сработать, в каком бы месте вашего окна пользователь его не нажал).
Самым интересным свойством карты входных событий является ее свойство поддерживать предков. В классе InputMap определен метод setParent(), который позволяет задать для карты ее предка. Означает это следующее: если поиск в карте клавиатурного сокращения результатов не дал, карта просит своего предка (если он есть) провести поиск того же сокращения, у предка в свою очередь может быть свой предок, и поиск клавиатурного сокращения происходит по всей иерархии карт. Это свойство полезно, когда вы не хотите полностью заменять карту входных событий какого-либо компонента, а вместо этого собираетесь всего лишь немного изменить ее, возможно даже на небольшой промежуток времени. Тогда имеющуюся карту можно сделать предком вашей новой карты, а затем при необходимости легко восстановить ее. К примеру, сложным текстовым компонентам в качестве основы проще использовать карты простых текстовых компонентов, потому что простые действия с текстом для них не меняются.
Карта второго типа, хранящаяся в классе JComponent, называется картой команд (action map). Это отображение некоторых объектов-ключей на классы, реализующие интерфейс Action (унаследованный от интерфейса ActionListener). Интерфейс Action предназначен для объектов, которые выполняют какое-то действие в компоненте. Мы подробнее обсудим этот интерфейс в главе 9, сейчас важно знать лишь то, что в нем есть метод actionPerformed(), который и вызывается для выполнения команды. Карты команд, реализованные классом ActionMap, содержат все команды, поддерживаемые компонентом. Чаще всего в карте команд в качестве ключей хранятся те же строки, что и в карте входных событий, и таким образом карты входных событий и карты команд связывают сообщения от клавиатуры и действия, которые они совершают. Аналогично картам входных событий, карты команд могут иметь предков. Для получения карты команд используется пара методов get/set.
«Почему же клавиатурные сокращения не связаны с командами напрямую, в одной карте?» — спросите вы. Здесь несколько причин. Во-первых, клавиатурные сокращения различны для трех состояний компонента, в то время как в карте команд просто хранятся все поддерживаемые компонентом команды. Во-вторых, так проще изменять поведение компонента: вы можете заменить клавиатурное сокращение, не изменяя команды, которое оно выполняет, а можете изменить команду, оставив клавиатурное сокращение тем же самым. Ну и, в-третьих, как мы уже отмечали, карты InputMap и ActionMap позволяют использовать предков, что может быть очень полезным. Не стоит забывать и о том, что у раздельных карт есть «запас прочности»: в будущем Swing может использовать их не только для клавиатурных сокращений.
Методы поддержки клавиатурных сокращений
Итак, как уже отмечалось, поддержка клавиатурных сокращений обеспечивается базовым классом библиотеки JComponent, который переопределяет метод processKeyEvent() и заменяет стандартный механизм своей цепочкой обработки событий от клавиатуры. Действие происходит следующим образом: приходит событие от клавиатуры. Система обработки событий AWT вызывает для текущего (обладающего фокусом ввода) компонента метод processKeyEvent().
114 |
ГЛАВА 5 |
Если дело происходит в компоненте библиотеки Swing, вызывается метод processKeyEvent() класса JComponent. Прежде всего он вызывает базовую версию метода super.processKeyEvent(), которая распределяет события по слушателям (если, конечно, таковые были зарегистрированы для компонента). После этого JComponent смотрит, не было ли событие полностью обработано (то есть не указал ли на это какой-либо из слушателей, вызвав метод consume()). Если событие обработано, работа этого события от клавиатуры завершается. Таким образом, слушатели имеют приоритет на обработку события. Кстати, все события от клавиатуры всегда добираются до метода processKeyEvent() класса JComponent, даже если в компоненте не были зарегистрированы слушатели, а значит, события соответствующего типа не добавлены в маску компонента (маскирование событий мы обсуждали в главе 3). Следит за этим конструктор класса JComponent, в котором в маску компонента добавляются события от клавиатуры (методом enableEvents()).
Далее, если ни слушатели, ни сам компонент так и не проявили интереса к событию, приходит пора проверить, не является ли это событие клавиатурным сокращением, зарегистрированным в компоненте. Для этого прежде всего запрашивается специальный внутренний класс KeyboardState, определенный в классе JComponent. Функция его проста — он отслеживает нажатие любых сочетаний клавиш и позволяет избежать срабатывания некоторого сочетания клавиш в том случае, если оно было нажато в другом окне или в другом приложении, а отпущено уже в нашем приложении. Обслуживаются лишь те сочетания клавиш, которые нажимаются пользователем в окнах нашего приложения. Если никаких препятствий для обслуживания сочетания клавиш не выявлено, управление передается методу processKeyBindings().
Метод processKeyBindings() приступает непосредственно к поиску зарегистрированных в компоненте клавиатурных сокращений и проверке того, не соответствует ли пришедшее от клавиатуры событие одному из этих сокращений. Помогают ему в этом класс KeyStroke, а также карты входных событий и команд.
Метод processKeyBinding() проверяет, не зарегистрировано ли в карте входных событий для состояния WHEN_FOCUSED пришедшее клавиатурное сокращение. Если оно там есть, метод получает из карты объект, соответствующий сокращению, и с его помощью пытается найти в карте команд соответствующую команду. Если такая команда есть, она выполняется, и работа завершается. (Кстати, вы можете переопределить метод processKeyBinding(), если захотите реализовать в своем компоненте особенный способ обработки сокращения.)
Если сокращение не было обработано, начинается опрос родительских компонентов. Для каждого из них также вызывается метод processKeyBinding(), но на этот раз ему указывают, что компонент находится в состоянии WHEN_ANCESTOR_OF_FOCUSED_ COMPONENT, так что поиск производится в соответствующей карте входных событий.
Таким образом, базовый класс JComponent вместе с помощниками заботится о поддержке компонентами Swing клавиатурных сокращений. Механизм клавиатурных сокращений используется практически всеми компонентами, начиная от кнопок и заканчивая текстовыми элементами. Там, где к нему обращаются очень часто, нам предоставляют более удобные способы регистрации клавиатурных сокращений (например, мнемоники для кнопок или клавиши быстрого доступа для команд меню), но и для остальных компонентов использовать их несложно. Более того, это единственный способ надежно обработать событие от клавиатуры, не происходящее непосредственно в самом компоненте. Напоследок рассмотрим небольшой пример:
//KeyBindingTest.java
//Пример использования клавитурных сокращений
import javax.swing.*; import java.awt.event.*;