Портянкин И. Swing
.pdf
Уход за деревьями |
475 |
Мы создаем небольшое окно с рамкой, в центре этого окна разместится дерево JTree (как и всегда, дерево предварительно вставлено в панель прокрутки JScrollPane). Данные дерево черпает из несложной модели, которая создается методом createTreeModel(). Модель состоит из пары узлов (корня и его потомка), к которым присоединены три листа, в листьях и располагаются телефонные номера. Чтобы номера было удобно присоединять к узлам (в цикле), они хранятся в массиве строк с названием phoneDirectory.
Специальный редактор для дерева с телефонными номерами описан во внутреннем классе с названием MaskTreeEditor. Данный класс унаследован от абстрактного базового класса AbstractCellEditor (в нем описана большая часть методов интерфейса CellEditor) и реализует интерфейс TreeCellEditor. Интерфейс TreeCellEditor на самом деле унаследован от интерфейса CellEditor, но методы CellEditor, описанные в базовом классе AbstractCellEditor, «считаются» методами TreeCellEditor, которые нам нужно реализовать, поэтому работы остается сделать не так уж и много. Прежде всего мы настраиваем компонент JFormattedTextField, который и будет применяться для редактирования значений, находящихся в узлах дерева. Это делается в конструкторе класса: в блоке try/catch создается форматирующий объект MaskFormatter, описывающий телефонный номер (подробнее о форматирующих объектах мы узнаем в главе 16), который затем передается в поле JFormattedTextField. Обратите внимание, что конструктору класса MaskFormatter требуется передать ссылку на дерево, с которым он будет работать. Чуть позже мы увидим, зачем это нужно. К созданному текстовому полю тут же присоединяется слушатель ActionListener, который оповещается об окончании редактирования. Как только мы получаем подобное оповещение, то вызываем метод stopCellEditing(), описанный в базовом классе AbstractCellEditor. Реализация базового класса при вызове данного метода сообщает слушателям CellEditorListener о том, что редактирование закончено.
Далее мы реализуем метод getTreeCellEditorComponent(), который должен возвратить компонент, используемый для редактирования. Здесь все просто: мы помещаем в текстовое поле строковое значение узла (подразумевая, что в узле хранится правильный телефонный номер) и возвращаем ссылку на это текстовое поле.
Следующая пара методов относится к интерфейсу CellEditor. Метод getCellEditorValue() возвращает значение, находящееся в данный момент в редакторе. В нем мы передаем текст, находящийся в текстовом поле. Метод isCellEditable() определяет, можно ли проводить редактирование для некоторого события EventObject, переданного ему в качестве параметра. Событие мы не анализируем, а просто получаем выделенный узел дерева (ссылку на дерево, как вы помните, мы сохранили в конструкторе) и разрешаем редактирование только для листьев дерева, так как в нашем случае телефонные номера хранятся только в листьях.
После написания редактора остается присоединить его к дереву. Мы включаем для дерева возможность редактирования, а дальше делаем все то же самое, что и в предыдущем примере: создаем стандартный отображающий объект и стандартный редактор DefaultTreeCellEditor, которому передаем ссылки на дерево, отображающий объект и наш новый редактор.
476 |
ГЛАВА 15 |
Запустив программу с примером, вы оцените удобство работы с новым, специализированным редактором узлов дерева. Редактировать можно только листья дерева с телефонными номерами, причем вводить разрешается лишь правильные телефонные номера: в противном случае вы просто не сможете передать новое значение дереву (редактирование, как и в предыдущем примере, включается тройным щелчком мыши или двойным щелчком с задержкой). В деревьях, предназначенных для хранения и отображения сложных данных, специальные редакторы незаменимы, во много раз повышая удобство работы с интерфейсом.
В дальнейшем, если вам понадобятся более сложные условия редактирования, вы сможете настроить их с помощью вспомогательных методов из интерфейса CellEditor, позволяющих анализировать событие в интерфейсе, данные и само дерево, прежде чем передавать дереву компонент для редактирования.
Дополнительные события деревьев
Пока мы увидели только одни события у деревьев, а именно события, сообщающие об изменении выделенных узлов, да и те скорее были у модели выделения дерева. Однако есть и еще кое-что. Мы можем узнать о моменте развертывания и свертывания узлов дерева, причем как уже о свершившемся факте, так и о моменте непосредственно перед тем, как узел будет развернут. Для этого служат слушатели TreeExpansionListener и TreeWillExpandListener, соответственно. Их присоединяют непосредственно к дереву JTree. Если первое событие полностью информационное, то второе позволяет нам предотвратить развертывание (или свертывание) определенного узла, если вдруг это понадобится. Рассмотрим простой пример:
//TreeExpansionEvents.java
//События при развертывании узлов дерева import javax.swing.*;
import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeWillExpandListener; import javax.swing.event.TreeExpansionEvent; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeModel;
import javax.swing.tree.ExpandVetoException; import java.awt.*;
public class TreeExpansionEvents extends JFrame { public TreeExpansionEvents() {
super("TreeExpansionEvents"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создаем дерево на основе модели
JTree tree = new JTree(createTreeModel()); // добавляем слушателей
TreeListener listener = new TreeListener(); tree.addTreeExpansionListener(listener); tree.addTreeWillExpandListener(listener);
Уход за деревьями |
477 |
add(new JScrollPane(tree)); setSize(400, 300); setVisible(true);
}
//листья дерева храним в массивах private String[] langs = {
"<html><b>Java", "<html><pre>Scala", "Ruby" }; private String[] ides = {
"IDEA", "<html><i>Eclipse", "NetBeans" };
//создание несложной модели дерева
private TreeModel createTreeModel() {
//корень нашего дерева
DefaultMutableTreeNode root = new DefaultMutableTreeNode(
"<html><font color=blue>Создание кода");
//основные ветви
DefaultMutableTreeNode lang =
new DefaultMutableTreeNode("Языки"); DefaultMutableTreeNode ide =
new DefaultMutableTreeNode("Среды"); root.add(lang);
root.add(ide);
// присоединяем листья
for (int i=0; i<langs.length; i++) { lang.add(new DefaultMutableTreeNode(langs[i])); ide.add(new DefaultMutableTreeNode(ides[i]));
}
// создаем стандартную модель return new DefaultTreeModel(root);
}
// слушатель событий о развертывании узлов
class TreeListener implements TreeExpansionListener, TreeWillExpandListener {
public void treeExpanded(TreeExpansionEvent event) { System.out.println("Узел развернут: " + event.getPath());
}
public void treeCollapsed(TreeExpansionEvent event) { System.out.println("Узел свернут: " + event.getPath());
}
public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { }
478 |
ГЛАВА 15 |
public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
if ( event.getPath().getLastPathComponent(). toString().equals("Языки") )
throw new ExpandVetoException(event);
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new TreeExpansionEvents(); } });
}
}
Начальная часть примера нам уже хорошо знакома из материала этой главы — мы добавляем дерево, обернутое в панель прокрутки, в центр окна, а для данных применяем стандартную модель дерева и стандартные узлы, данные для которых находятся в массивах. Самое интересное находится в слушателях, которых мы присоединяем к дереву. Для простоты оба они реализованы одним и тем же внутренним классом.
Методы слушателя TreeExpansionListener вызываются уже «постфактум», так что мы можем использовать этот факт только в качестве полезной информации. В примере мы так и поступаем, просто выводя путь к развернутому или свернутому узлу на консоль. Заметьте, что путь развернутого узла можно получить прямо из события с помощью метода getPath().
Методы же слушателя TreeWillExpandListener (их можно определить по слову «will» в названии) вызываются перед развертыванием узла, и что самое интересное, позволяют возбуждать исключение ExpandVetoException. Если вы создадите подобное исключение, для дерева это будет знаком того, что данный узел нельзя разворачивать, или, как в нашем примере, сворачивать. Чтобы определить, что это тот самый узел, мы проверяем его строковое значение, но это не самый гибкий способ, как правило, проще получить данные узла и каким-то образом их проверить. Запустив пример, вы поймете, что развернуть узел с названиями языков можно, а вот свернуть обратно никак не получится. Интересно, что корень дерева при этом все равно можно свернуть, поэтому если вы хотите удостовериться в том, что на экране виден важный для вас узел, имеет смысл следить не только за ним, но и за его узлами-предками.
Резюме
Деревья Swing позволяют выводить на экран любые данные, находящиеся в иерархических отношениях, даже очень сложных. Все аспекты работы дерева (возможность редактирования, внешний вид узлов и листьев, режимы выделения и многое другое) полностью управляемы, и вы сможете работать с деревом именно так, как это нужно вашему приложения и как это удобно вам.
Глава 16. Текстовые компоненты
Одной из самых впечатляющих частей библиотеки Swing является пакет javax.swing. text, обеспечивающий ее инструментами для работы с текстом. Благодаря этому пакету в вашем арсенале появляются несколько мощных текстовых компонентов, позволяющих реализовать в приложении средства ввода и редактирования текста любой сложности, начиная от однострочного текстового поля, часто применяемого для получения от пользователя простой информации, и заканчивая многофункциональным текстовым редактором с разнообразными возможностями. Учитывая, что текстовые компоненты используются в приложениях очень часто, мощь и гибкость текстовых компонентов библиотеки Swing будут как нельзя кстати. И действительно, трудно представить себе приложение, в котором нет текстовых компонентов, предназначенных для ввода информации пользователем. В том или ином виде, но текстовые компоненты, в том числе и встроенные в другие компоненты, присутствуют в приложении всегда.
Пакет javax.swing.text тщательно спланирован, качественно реализован и без сомнения очень велик и довольно сложен. Его объем и сложность его изучения вполне сравнимы с объемом и сложностью изучения самой библиотеки Swing, если иметь в виду все ее компоненты и возможности, а не только относящиеся к обработке текста. Не погрешив против истины, можно сказать, что текстовый пакет представляет собой небольшое «королевство» внутри Swing. Тем не менее, как и всегда в случае с библиотекой Swing, вы можете добиться очень многого, не прикладывая для этого титанических усилий и не вникая во все тонкости реализации текстового пакета Swing. Как мы вскоре увидим, несколькими строками кода можно создать вполне приличные инструменты для редактирования текста, достаточные для широкого класса приложений.
Основные возможности всех текстовых компонентов Swing и их базовая архитектура описаны в абстрактном классе JTextComponent из пакета javax.swing.text. Именно от этого класса унаследованы все текстовые компоненты Swing, будь то простое текстовое поле или многофункциональный редактор. Помимо того что в данном базовом классе задаются общие для всех текстовых компонентов свойства и действия (такие как цвет выделенного текста, цвет самого выделения, курсор, сам текст, механизмы работы с буфером обмена), в нем описывается взаимодействие практически всех составных частей пакета javax.swing.text, которых в нем насчитывается очень и очень много. Постепенно, по мере чтения этой главы, мы будем узнавать самое важное о механизмах текстового пакета Swing, пока же стоит отметить, что в текстовых компонентах Swing, в отличие от остальных компонентов библиотеки, модель, вид и контроллер практически полностью разделены. Как известно, текстовые компоненты довольно сложны и состоят из многих элементов. Оказалось, что раздельные элементы архитектуры MVC подходят для них как нельзя лучше. Модель текстовых компонентов представлена довольно простым интерфейсом Document, который позволяет получать информацию об изменениях в документе и хранящийся в нем текст, а также при необходимости изменять полученный текст. Вид, как нетрудно догадаться, в конечном итоге реализован в UI-представителях текстовых компонентов, но составляется он на основе специальных объектов Element и View, больше отвечающих именно текстовым компонентам. Благодаря этим объектам вы сможете гибко настраивать и расширять внешний вид и структуру текстовых компонентов без вмешательства в сложный процесс их конечной прорисовки. Контроллер частично соединен с видом (особенно это касается событий, не связанных с клавиату-
480 |
ГЛАВА 16 |
рой), а частично реализован в виде карты клавиатуры (keymap), аналога карты входных событий, известной нам еще с главы 5. Карта клавиатуры позволяет гибко, без смены UI-представителя текстового компонента, менять реакцию текстового компонента на нажатия клавиш (это очень важно, поскольку даже в одном текстовом компоненте могут поддерживаться несколько видов документов, способы редактирования которых возможно кардинально отличаются) и таким образом представляет собой ограниченный вариант контроллера.
Тем не менее, для будничной работы с текстовыми компонентами, за рамки которой большинство приложений не выходит, нам не придется вникать в детали реализации текстового пакета и тонко настраивать его механизмы. Познакомимся теперь с текстовыми компонентами Swing и убедимся, что работать с ними не сложнее, чем с другими компонентами библиотеки.
Каталог текстовых компонентов
Как мы уже отмечали, Swing предоставляет нам целый арсенал текстовых средств, от небольших полей для ввода простого текста до полнофункциональных редакторов со всеми мыслимыми возможностями. Прежде чем перейти к более детальному изучению их возможностей (большинство из которых для всех текстовых компонентов одинаковы), составим своеобразный каталог текстовых компонентов Swing с небольшими примерами, которые позволят нам быстро познакомиться с ними. Кроме того, данный каталог удобно будет держать «под рукой» в качестве простого справочника.
Текстовые поля
Текстовые поля (text fields) — самые простые и наиболее часто встречающиеся в пользовательских интерфейсах текстовые компоненты. Как правило, такое поле является однострочным и служит для ввода текста. В библиотеке Swing имеется два текстовых поля. Первое, представленное классом JTextField, позволяет вводить некоторый простой однострочный текст (разнообразные атрибуты форматирования, такие как дополнительные цвета и шрифты, не поддерживаются). Второе поле, реализованное классом JPasswordField и унаследованное от простого поля JTextField, дает возможность организовать ввод «секретной» информации (чаще всего паролей), которая не должна напрямую отображаться на экране. Оба текстовых поля очень просты, работа с ними чаще всего сводится к заданию количества отображаемых в поле символов и начального текста (если таковой требуется), после чего остается только поместить поле в контейнер и в нужный момент получить из него набранный пользователем текст. Рассмотрим простой пример использования текстовых полей Swing:
//UsingTextFields.java
//Использование текстовых полей Swing import javax.swing.*;
import java.awt.*; import java.awt.event.*;
public class UsingTextFields extends JFrame { // наши поля
private JTextField smallField, bigField; public UsingTextFields() {
super("UsingTextFields");
Текстовые компоненты |
481 |
setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем текстовые поля smallField = new JTextField(10);
bigField = new JTextField("Текст поля", 25);
//дополнительные настройки
bigField.setFont(new Font("Verdana", Font.PLAIN, 16)); bigField.setHorizontalAlignment(JTextField.RIGHT);
// слушатель окончания ввода smallField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { // показываем введенный текст
JOptionPane.showMessageDialog(
UsingTextFields.this, "Ваше слово: " +
smallField.getText());
}
}); // поле с паролем
JPasswordField password = new JPasswordField(15); password.setEchoChar('$');
// добавляем поля в окно и выводим его на экран setLayout(new FlowLayout());
add(smallField);
add(bigField);
add(password); setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingTextFields(); } });
}
}
Здесь мы создаем окно небольших размеров с несколькими текстовыми полями. Первое поле создается с помощью наиболее распространенного варианта конструктора класса JTextField, которому необходимо передать максимальное количество символов в поле. Для однострочных текстовых полей прокрутка не нужна, и размер поля (в символах) должен примерно соответствовать объему информации, которую пользователь вводит в поле. В противном случае работать с текстовым полем было бы не совсем удобно, причем как при нехватке места для вводимой информации, так и при избыточной длине поля. В нашем примере особенных требований к вводимой информации нет, так что мы вполне можем указать произвольное количество символов. Второе поле созда-
482 |
ГЛАВА 16 |
ется более функциональным конструктором: ему необходимо передать текст, который будет записан в поле, и максимальное количество символов (для примера мы делаем это значение достаточно большим). Далее демонстрируются дополнительные возможности текстовых полей, довольно примитивные, но вполне уместные: вы можете с легкостью сменить шрифт и вариант выравнивания текста в поле (по умолчанию текст выравнивается по левому краю, мы выровняли его по правому краю, таким же образом можно задать выравнивание текста по центру).
Обратите внимание на то, что к текстовому полю можно присоединить слушателя событий ActionListener. Такие слушатели оповещаются о нажатии пользователем специальной клавиши, сигнализирующей об окончании ввода (обычно это клавиша Enter). Использовать слушателя особенно удобно в случае текстовых полей, предназначенных для ввода важной информации, такой как имя пользователя в диалоговом окне входа в систему. Присоединение к полю слушателя ActionListener ускоряет процесс работы с интерфейсом, так как избавляет пользователя от необходимости каждый раз по окончании ввода данных щелкать на подтверждающих кнопках (подобных кнопке OK) — вместо этого он сразу же, не покидая текстового поля, нажимает клавишу Enter и быстро переходит к следующему этапу работы с приложением. Помимо прямого присоединения к полю слушателя ActionListener вы можете воспользоваться методом setAction(), присоединяющего к полю объект-команду Action (применение этого метода не удаляет уже присоединенных к полю слушателей, все они также будут оповещаться о завершении ввода).
Пример также иллюстрирует работу с полем для ввода «секретных» данных JPasswordField. Не забывайте, что данное поле унаследовано от обычного поля JTextField, так что все вышесказанное верно и для него. Из собственных методов поля JPasswordField можно упомянуть лишь метод setEchoChar(), служащий для смены символа-заменителя (символа, который отображается вместо всех вводимых пользователем в поле символов во избежание появления на экране конфиденциальной информации). По умолчанию в качестве такого символа используется звездочка (*), которую для примера мы заменили символом доллара ($). Разработчики класса JPasswordField не рекомендуют также применять для получения введенного в поле значения (пароля) обычный метод getText(). Дело в том, что создаваемая данным методом строка String не может меняться все время жизни программы (объекты String в Java максимально оптимизируются компилятором и виртуальной машиной), и вы не сможете «стереть» ее из памяти. Для получения данных предоставляется более безопасный метод getPassword(), возвращающий массив символов char (значения которого после проверки имеет смысл обнулить и при желании вызвать сборщик мусора). Поле JPasswordField также особым образом копирует данные в буфер обмена — оно переопределяет методы cut() и copy() (эти методы определены в базовом классе JTextComponent), запрещая копировать набранный текст в буфер обмена, что также служит для защиты конфиденциальной информации.
Запустив программу с примером, вы сможете увидеть работу текстовых полей воочию. Подвести итог нам поможет табл. 16.1 с описанием основных свойств текстовых полей Swing.
Текстовые компоненты |
483 |
|
Таблица 16.1. Свойства текстовых полей |
|
|
|
|
|
Свойства |
Описание |
|
(и методы get/set) |
|
|
text |
Позволяет получить введенный в поле текст или заменить его. Для по- |
|
|
ля с конфиденциальной информацией лучше использовать метод |
|
|
getPassword() |
|
columns |
Задает количество столбцов в поле, так что вы можете получить размер |
|
|
поля или в любой момент изменить его |
|
font |
Определяет используемый в текстовом поле шрифт. Не стоит слишком |
|
|
вольно распоряжаться этим свойством, так как текстовые поля встреча- |
|
|
ются в интерфейсе часто и должны органично вливаться в общий стиль |
|
|
внешнего вида приложения. Экзотичные шрифты, особенно больших раз- |
|
|
меров, могут этому помешать |
|
horizontalAlignment |
Управляет выравниванием текста в поле. По умолчанию текст выравни- |
|
|
вается по левой границе поля, вы можете выбрать для текста любой из |
|
|
трех вариантов выравнивания (слева, справа или по центру) |
|
echoChar (только для |
Задает символ-заменитель для ввода секретной информации. По умол- |
|
класса JPassword- |
чанию используется символ звездочки (*) |
|
Field) |
|
|
И еще одно замечание: если вы помните, в главе 7 при изучении блочного расположения BoxLayout мы столкнулись с неприятной проблемой, связанной с текстовым полем JTextField: при использовании менеджера расположения, учитывающего максимальный размер компонентов, поле «раздувается», так как максимальный размер его не ограничен. Свойства неограниченности максимального размера унаследовано классом JTextField от своего предка JTextComponent. Для всех остальных текстовых компонентов оно подходит как нельзя лучше — вы можете вводить в такие компоненты неограниченное количество символов текста (все упирается только в доступную вашему приложению память), но для однострочных полей JTextField и JPasswordField это не так. В рассмотренном нами примере использовалось последовательное расположение FlowLayout (в панелях JPanel оно устанавливается по умолчанию), в котором учитывается только предпочтительный размер компонента, так что с ним вопросов не возникает. Ну а трудности с другими менеджерами расположения мы решили в главе 7, написав для исправления размера текстового поля вспомогательный инструмент.
Многострочное поле JTextArea
Многострочное текстовое поле JTextArea представляет собой немного более расширенную версию обычного текстового поля JTextField (хотя класс JTextArea не унаследован от класса JTextField, слишком уж различна их реализация). Как и обычное поле JTextField, многострочное поле JTextArea предназначено для ввода простого неразмеченного различными атрибутами текста, но в отличие от обычных полей, позволяющих вводить только одну строку текста, многострочные поля дают пользователю возможность вводить произвольное количество строк текста. Кроме того, поле JTextArea часто используется для вывода разного рода подробной информации о работе приложения, отображать которую в диалоговых окнах или в консоли неудобно. Вывод информации в многострочное текстовое поле позволяет пользователю без труда просмотреть ее, как бы много ее ни было, и при необходимости скопировать или изменить.
Работают с многострочными полями так же, как с обычными, за тем исключением, что для них приходится задавать не только ширину (максимальное количество символов), но и высоту (максимальное количество строк). Многострочные текстовые поля
484 |
ГЛАВА 16 |
следует размещать в панелях прокрутки JScrollPane, иначе при вводе множества строк текста они «расползутся» по контейнеру и испортят вид остальных компонентов. Рассмотрим несложный пример и познакомимся с основными возможностями класса
JTextArea:
//UsingTextArea.java
//Использование многострочных полей import javax.swing.*;
import java.awt.*;
public class UsingTextArea extends JFrame { public UsingTextArea() {
super("UsingTextArea"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем пару многострочных полей
JTextArea area1 = new JTextArea( "Многострочное поле", 5, 10);
//нестандартный шрифт и табуляция area1.setFont(new Font("Dialog", Font.PLAIN, 14)); area1.setTabSize(10);
JTextArea area2 = new JTextArea(15, 10);
//параметры переноса слов area2.setLineWrap(true); area2.setWrapStyleWord(true);
//добавим поля в окно
setLayout(new FlowLayout()); add(new JScrollPane(area1)); add(new JScrollPane(area2)); // выводим окно на экран setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingTextArea(); } });
}
}
В примере мы размещаем в окне пару многострочных текстовых полей JTextArea, для которых были изменены некоторые наиболее интересные свойства, чаще других использующиеся в приложениях. Первое текстовое поле создается с помощью довольно функционального конструктора, задающего для поля находящийся в нем текст, количество строк и символов. Обратите внимание, что количество строк идет в списке параметров перед количеством символов; здесь часто возникает путаница, так как для
