
Портянкин И. Swing
.pdfСтандартные диалоговые окна |
435 |
(методом setPreviewPanel()), в которой принято показывать, как смена цвета скажется на работе пользователя еще до того, как он окончательно сделает свой выбор. В панели просмотра вы можете показать, к примеру, уменьшенную копию изображения с новыми цветами. Для того чтобы узнать о смене цвета в компоненте JColorChooser еще до окончательного выбора, используйте модель выбора цвета ColorSelectionModel. Присоединив к этой модели слушателя ChangeListener, вы будете узнавать, какие цвета «опробует» пользователь перед тем, как принять окончательное решение.
Резюме
Стандартные диалоговые окна легко настраивать и выводить на экран, они хорошо знакомы пользователю и наглядно показывают ему, что ваше приложение не сложнее и не запутаннее других. Использование в подходящих местах стандартных диалоговых окон упрощает и ускоряет не только процесс знакомства пользователя с приложением, но и жизнь программиста, позволяя выводить и получать информацию всего несколькими строками кода.
Глава 15. Уход за деревьями
Разнообразную информацию можно представить в виде иерархических отношений «родитель-потомок», когда некоторые «младшие» данные хранятся как часть «старших» данных. «Старшие» данные в свою очередь могут иметь своих «родителей», и так может продолжаться бесконечно. Иерархические отношения данных принято отображать в специальных компонентах пользовательского интерфейса, деревьях (trees), идея которых навеяна генеалогическими деревьями, издавна используемыми для отображения семейных отношений. Компьютерное дерево и на самом деле похоже на дерево, только поставленное с «ног» на «голову»: вверху располагается корень (root), которого, впрочем, может и не быть,
аниже находятся его потомки (первые ветви), у которых могут быть свои потомки, и т.д.
Удеревьев, используемых в мире компьютеров, есть своя нехитрая терминология. Информация о ветвях дерева хранится в специальных объектах, узлах (nodes). Узел, у которого нет потомков, справедливо называется листом (leaf) дерева. Все узлы-потомки одного узла-предка называются элементами одного уровня (siblings). Путь (path) в дереве определяет, как добраться до некоторого узла, и представляет собой последовательность узлов, начиная от корня, по которым следует пройти, чтобы найти нужный нам узел.
Деревья в Swing реализованы компонентом JTree. Класс JTree буквально напичкан различными методами, более того, существует целый вспомогательный пакет javax.swing.
tree с добрым десятком классов и интерфейсов, обеспечивающих правильную работу деревьев Swing. Это неудивительно: деревья Swing обладают впечатляющими возможностями, и все что возможно делать с деревьями, они делать позволяют. Пакет javax.swing. tree хранит основные «строительные кирпичики» деревьев Swing: интерфейс TreeNode описывает узел дерева; модель дерева, хранящая его данные, описана интерфейсом TreeModel; путь в дереве описывается объектом TreePath. Пока нам этого хватит, ну а по мере чтения этой главы мы узнаем и об остальных частях деревьев Swing.
Впрочем, как и всегда в Swing, сказанное не означает, что вам придется долго и тщательно изучать возможности класса JTree, прежде чем «вырастить» даже самое простое дерево. Простые, но довольно эффектные деревья вполне можно создать несколькими строками кода, а все остальные их возможности вы будете привлекать только по мере необходимости. Итак, начнем по порядку.
Простые деревья
Для вывода несложных деревьев на экран не обязательно окунаться в хитросплетения модели деревьев TreeModel или начинать изучение узлов TreeNode, вы можете создавать простые деревья с помощью нескольких вспомогательных конструкторов класса JTree. Правда, возможности созданных этими конструкторами деревьев довольно ограничены, потому что иерархическая структура данных деревьев весьма специфична, и полностью представить ее простыми данными (динамическими или ассоциативными массивами) не так просто, как, к примеру, в случае со списками. Тем не менее, способа создать быстрее настоящее дерево не существует, так что конструкторы эти стоит держать «под рукой».
В качестве источника данных при создании деревьев с помощью простых конструкторов можно использовать одномерные массивы, динамические массивы Vector или ассоциативные массивы Hashtable. Мы уже располагаем опытом работы с библиотекой Swing (если вы прочли все предыдущие главы, конечно), поэтому не трудно догадаться,
Уход за деревьями |
437 |
что на самом деле незаметно для нас эти данные «оборачиваются» в некую стандартную модель дерева JTree. Особую роль в создании деревьев на основе разнообразных массивов играет специальный тип узла дерева DynamicUtilTreeNode. Он позволяет быстро соорудить список узлов-потомков для какого-либо узла на основе массива (простого или динамического), причем потомки эти добавляются к узлу только при его раскрытии пользователем, так что если узел никогда не раскроется, потомков у него не будет.
Ну а теперь можно рассмотреть пример использования конструкторов класса JTree, позволяющих быстро наполнить дерево данными:
//SimpleTrees.java
//Создание самых простых деревьев import javax.swing.*;
import java.util.*;
public class SimpleTrees extends JFrame { public SimpleTrees() {
super("SimpleTrees"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создание дерева на основе массива
Object[] data = new Object[] { "Первый", "Второй", "Третий", new String[] { "Чей-то потомок",
"Еще потомок" }
};
JTree tree1 = new JTree(data);
//дерево на основе вектора
Vector vector = new Vector();
for (int i=0; i<5; i++) vector.add("Лист № " + i); JTree tree2 = new JTree(vector);
//дерево на основе таблицы
Hashtable table = new Hashtable(); table.put("Одна", "пара"); table.put("Еще одна", "тоже пара"); JTree tree3 = new JTree(table);
//можно включить показ корня дерева tree3.setRootVisible(true);
//добавляем деревья в панель содержимого
JPanel contents = new JPanel(); contents.add(tree1); contents.add(tree2); contents.add(tree3); setContentPane(contents);
//выводим окно на экран
setSize(400, 300); setVisible(true);

438 |
ГЛАВА 15 |
}
public static void main(String[] args) { new SimpleTrees();
}
}
Мы создаем три разных дерева с помощью разных конструкторов класса JTree: первое — на основе обычного массива, второе — на основе динамического массива (вектора) Vector, в третьем для наполнения дерева данными используется таблица (ассоциативный массив) Hashtable. Все просто: заполнив подходящий контейнер данными, вы передаете этот контейнер в конструктор и размещаете на экране созданное дерево. Заметьте, что мы задействовали для первого дерева вложенные массивы объектов — это возможно, поскольку массив в Java также является обычным объектом. В результате мы получим не только листья, относящиеся к корню дерева, но и новые узлы с вложенными в них потомками, вот только, к сожалению, у нас нет возможности задать для этих узлов названия, так что вместо названий будут использованы адреса соответствующего массива в памяти, а это вряд ли восхитит пользователя. Так что вложенные массивы, скорее всего, не пригодятся вам при создании простых деревьев, хотя с их помощью можно быстро создавать ветви самой разной степени сложности (вы также можете вкладывать в массив списки Vector для создания отдельных ветвей).
Аналогичным образом обстоит дело и с динамическими массивами: вы также можете использовать в качестве их элементов такие же массивы, но отображаться на экране они будут как адреса в памяти, так что лучше эту затею оставить (как мы вскоре увидим, проще обратиться к модели дерева). Вектор мы заполняем элементами (будущими листами дерева) динамически, в цикле, а в таблице размещаем две пары «ключ-значение». Запустив программу спримером,выувидите,чтонаэкранвыводятсятолькоключи,апрозначенияможнозабыть, причем ключи могут появиться на экране в произвольном порядке, а не в том, в котором вы их добавляли в таблицу; все зависит от того, как пары физически хранятся в таблице.
Все простые деревья, создаваемые применяемыми в примере конструкторами класса JTree, не показывают корень дерева, так что дерево на самом деле на дерево не слишком похоже (оно скорее напоминаем своеобразный список). Вы можете вывести корень на экран с помощью свойства rootVisible, что мы и сделали для третьего дерева в примере, но при этом название корня будет стандартным («root»), потому что мы не можем задать его в наших простых конструкторах. Впрочем, вы сможете исправить ситуацию после изучения возможностей стандартной модели дерева, которая неявно используется классом JTree в нашем примере: стандартная модель позволяет изменить названия всех узлов дерева и после его создания.

Уход за деревьями |
439 |
В итоге все три созданных нами дерева помещаются в окно (с последовательными расположением FlowLayout) и выводятся на экран. Заметьте, что мы не задействовали панель прокрутки именно из-за последовательного расположения, для которого потребовалось бы слишком много места (панели прокрутки с деревьями довольно требовательны к месту, даже если дерево невелико). При использовании для дерева панели прокрутки лучше проектировать интерфейс таким образом, чтобы панель прокрутки занимала в контейнере определенную фиксированную область. Без панелей прокрутки деревья ведут себя не лучшим образом: раскрытие или свертывание любого узла приводит к изменению всего интерфейса, что для пользователя станет сюрпризом, так что в дальнейшем мы всегда будем «оборачивать» деревья в панели прокрутки
JScrollPane.
Из всех компонентов библиотеки Swing деревья, пожалуй, единственные не позволяют создать более или менее приличный компонент с помощью простого конструктора (согласитесь, что созданные нами в примере деревья больше напоминают необычные списки). Иногда такие деревья могут вам пригодиться, но все же требуются они редко. Так что не будем медлить и перейдем к рассмотрению модели дерева.
Модель дерева TreeModel
Хотя простые деревья и можно создавать с помощью массивов данных и конструкторов класса JTree, на настоящие деревья они похожи мало. Показать иерархические данные любой сложности дерево сможет, лишь черпая эти данные из специально предназначенной для этого модели TreeModel. Модель дерева TreeModel позволяет описать любую иерархическую структуру данных с единственным корнем. С ее помощью можно указать, сколько потомков имеется у определенного узла дерева, получить потомка узла по его порядковому номеру и наоборот, получить порядковый номер потомка по его значению, легко выяснить, является ли некоторый узел листом дерева. Кроме того, модель TreeModel поддерживает списки слушателей TreeModelListener, которые оповещаются при изменениях в модели. Всего этого оказывается вполне достаточно для вывода компонентом JTree самых пышных деревьев.
В качестве узлов дерева в модели TreeModel применяются самые обычные объекты Object, так что использовать для хранения информации об узлах в своей реализации модели дерева можно все что угодно (любой объект Java унаследован от Object). Давайте попробуем написать несложную модель дерева с обычными строками в качестве узлов, которые и будут выводиться деревом на экран1. А для того чтобы упростить поддержку древовидной структуры, хранить эти строки мы будем в динамических списках ArrayList (так проще определять порядковые номера потомков). Вы тут же можете спросить: «А как же стандартная модель, наверное, с ее-то помощью можно создать дерево быстрее и проще?». На самом деле, мы всегда сначала рассматривали стандартную модель в качестве более простого варианта размещения данных в модели, но с деревом все обстоит не так, не как обычно. Интерфейс модели дерева TreeModel позволяет описать иерархическую структуру с использованием любых объектов, и стандартная модель дерева выбирает в качестве таких объектов узлы, описываемые интерфейсом TreeNode. Мы вскоре с ними познакомимся, и тогда уже перейдем к стандартной модели дерева.
Ну а теперь, как мы и хотели, создадим собственную модель дерева «с нуля», реализуя интерфейс TreeModel:
//SimpleTreeModel.java
//Создание простой модели для дерева
1 По умолчанию в качестве узла дерева на экран выводится строка, полученная методом узла toString(). Впрочем, как мы вскоре выясним, это поведение настраивается.

440 ГЛАВА 15
import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import java.util.*;
import java.awt.*;
public class SimpleTreeModel extends JFrame { public SimpleTreeModel() {
super("SimpleTreeModel"); setDefaultCloseOperation(EXIT_ON_CLOSE); // дерево на основе нашей модели
JTree tree = new JTree(new SimpleModel()); // добавляем его в окно
add(new JScrollPane(tree)); setSize(300, 200); setVisible(true);
}
// наша модель для дерева
class SimpleModel implements TreeModel { // корень дерева и основные узлы
private String root = "Кое-что интересное"; private String
colors = "Цвета", food = "Еда";
// хранилища данных
private ArrayList<String> rootList = new ArrayList<String>(), colorsList = new ArrayList<String>(),
foodList = new ArrayList<String>(); public SimpleModel() {
// заполняем списки данными rootList.add(colors); rootList.add(food); colorsList.add("Красный"); colorsList.add("Зеленый"); foodList.add("Мороженое"); foodList.add("Бутерброд");
}
// возвращает корень дерева public Object getRoot() {
Уход за деревьями |
441 |
return root;
}
//сообщает о количестве потомков узла public int getChildCount(Object parent) {
if ( parent == root ) return rootList.size(); else if ( parent == colors )
return colorsList.size();
else if ( parent == food ) return foodList.size(); return 0;
}
//возвращает потомка узла по порядковому номеру public Object getChild(Object parent, int index) {
if ( parent == root )
return rootList.get(index); else if ( parent == colors )
return colorsList.get(index); else if ( parent == food )
return foodList.get(index); return null;
}
//позволяет получить порядковый номер потомка public int getIndexOfChild(
Object parent, Object child) { if ( parent == root )
return rootList.indexOf(child); else if ( parent == colors )
return colorsList.indexOf(child); else if ( parent == food )
return foodList.indexOf(child); return 0;
}
//определяет, какие узлы являются листьями
public boolean isLeaf(Object node) { if ( colorsList.contains(node) ||
foodList.contains(node) ) return true; else return false;
}
442 |
ГЛАВА 15 |
//вызывается при изменении значения некоторого узла
//для нашей модели не понадобится
public void valueForPathChanged( TreePath path, Object value) {
}
//методы для присоединения и удаления слушателей
//нашей простой модели не потребуются
public void addTreeModelListener( TreeModelListener tml) {
}
public void removeTreeModelListener( TreeModelListener tml) {
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new SimpleTreeModel(); } });
}
}
Уже по объему кода в нашем примере можно предположить, что даже такую простую модель для дерева, которую мы хотели создать здесь, на самом деле создать не так уж и просто (по крайней мере, приходится довольно много писать). Так оно и есть: не забывайте, что дерево хранит иерархические данные, и правильная организация таких данных и их гибкое хранение требуют от нас определенных стараний. Сам по себе пример очень прост: мы наследуем от окна JFrame, создаем на основе нашей модели дерево, добавляем его в центр окна (не забывая включить дерево в панель прокрутки JScrollPane, поскольку предыдущий пример показал, что без нее дерево не слишком-то изящно) и выводим окно на экран. Все самое интересное находится в классе модели SimpleModel.
Для того чтобы объект мог стать моделью для дерева, ему нужно реализовывать интерфейс TreeModel. В этом интерфейсе довольно много методов, применять большую часть которых непросто, поэтому в примере каждый метод снабжен подробным комментарием, так что вы не запутаетесь в его назначении. В качестве узлов в нашей модели используются обычные строки String. Заметьте, что данные любого узла, обладающего потомками, хранятся в списках ArrayList, которые позволяют с легкостью определять порядковые номера потомков или получать потомков по их порядковым номерам. Кроме того, с помощью списков мы сможем без труда определить, является ли узел листом (то есть узлом, не обладающим потомками): все листы у нас также находятся в соответствующих списках. Корнем дерева является строка root. Все принадлежащие корню дерева узлы-потомки хранятся в списке rootList (все списки мы наполняем в конструкторе). Корень дерева возвращает метод getRoot() модели. Следующие три метода служат для определения порядковых номеров потомков узла и получения самих потомков. Мы реализовали их с помощью удобных методов списка ArrayList. Заметьте, что даже у такого небольшого дерева, которое мы создаем в примере, для распознания узлов приходится использовать каскадные операторы if-else, что не придает коду изящества и гибкости. Следующий метод (метод isLeaf()) позволяет выяснить, является ли узел листом. Здесь

Уход за деревьями |
443 |
нам также помогают свойства списка ArrayList (все листы у нас хранятся в списках, так что мы можем легко найти их). Дереву JTree необходимо знать про листы, чтобы оптимизировать производительность модели и прорисовки.
Наконец, последние три метода для нашей простой модели излишни, так что реализовывать мы их не стали. Первый метод вызывается деревом при изменении значения некоторого узла (это может произойти, если ваше дерево допускает редактирование). В примере узлы дерева редактировать нельзя, так что в нашей модели этот метод ни к чему. Тем не менее, стоит помнить о том, что метод valueForPathChanged() вызывается при редактировании любого узла дерева. Если в ваших узлах хранятся нестандартные данные (даже если вы используете стандартную модель), данный метод имеет смысл переопределить и правильно изменять эти данные. Пример, показывающий, как и зачем это делается, мы рассмотрим при подробном обсуждении процесса редактирования узлов дерева.
Два заключительных метода служат для присоединения и отсоединения слушателей, которых мы должны оповещать при изменениях в данных нашей модели. Данные нашей модели во время работы программы не меняются, так что списки слушателей нам тоже не пригодятся (впрочем, мы еще вспомним об этой задаче чуть позже). Обратите внимание, что создатели модели дерева не предоставили нам абстрактный класс с названием вроде AbstractTreeModel, в который была бы встроена поддержка списков слушателей (аналоги подобных классов есть практически для всех компонентов Swing с моделями). Причина здесь все та же — дерево очень гибко, и создать класс для общих нужд не получается. Запустив программу с примером, вы увидите, как данные модели превращаются в дерево.
Согласитесь, что для таких простых данных, какие мы использовали в модели, пришлось выполнить чересчур много работы. У нас в дереве выводится всего один корень, два узла с двумя потомками, а написать пришлось порядочно. Без сомнения, обычные, неструктурированные данные мало подходят для описания древовидных структур. Очевидно, что в дополнение к самим данным необходимо добавлять ссылки и на их потомков, и так до окончательных листьев, иначе описание дерева превращается в пытку, а код в «спагетти».
Проще было бы раз и навсегда написать класс, позволяющий создавать любые сочетания узлов и листьев, который при необходимости сам сможет справиться с запросами модели и управлять всеми списками с данными, вставлять новые узлы и удалять уже имеющиеся. С одной стороны, можно доработать нашу простую модель, но использовать в качестве узлов простые строки не слишком разумно: чаще всего в деревьях хранятся куда более сложные данные. С другой стороны, нам стоит вспомнить о стандартной модели дерева DefaultTreeModel. Она позволяет делать с узлами и листьями все то, о чем мы говорили, и использует для хранения информации об узлах специальные объекты TreeNode, прекрасно для этого подходящие и позволяющие хранить любую ин-
444 |
ГЛАВА 15 |
формацию. Но забывать о реализации модели дерева «с нуля», как бы запутано это ни было, не стоит: если в вашем приложении данные хранятся в специальных древовидных структурах, будет проще написать собственную модель дерева, а не переносить данные в стандартную модель. Это позволит свести работу к минимуму и максимально оптимизировать ее, например, сообщать о потомках узла динамически, только когда этого потребует дерево. Ну а теперь познакомимся с узлами TreeNode.
Узлы TreeNode
Интерфейс TreeNode из пакета javax.swing.tree описывает характеристики единственного узла (в модели описание всех узлов «раскидано» сразу по нескольким методам, что и вносит в нее дополнительную сложность). В характеристики узла входят перечисление (Enumeration) его потомков и получение их по порядковому номеру (потомки возвращаются также в виде объектов TreeNode), а также информация о предке узла и о том, является ли данный узел листом. Таким образом, если в модели дерева TreeModel вы описываете все узлы сразу, то, реализуя интерфейс TreeNode (и в дальнейшем используя его в стандартной модели дерева), вы получаете возможность говорить только об одном узле. Без сомнения, это проще.
Впрочем, интерфейс TreeNode используется не так уж и часто. У него есть гораздо более популярный потомок — унаследованный от него интерфейс MutableTreeNode. Последний определяет еще несколько методов, позволяющих динамически добавлять к узлу новых потомков, удалять их (по номеру или по значению), менять предков узла или удалять данный узел из списка потомков предка. Кроме того, в этом интерфейсе есть метод setUserObject(), который позволяет быстро сменить данные, хранящиеся в узле. Данные могут иметь любой тип, так что в ваших узлах может храниться все что угодно, даже очень сложные данные (например, содержимое файлов, соответствующих узлу дерева).
Для использования возможностей узлов TreeNode (и впоследствии стандартной модели дерева)вамнепридетсяскрупулезнореализовыватьвсеметодыописанныхвышеинтерфейсов. Библиотека предоставляет нам стандартную реализацию интерфейса MutableTreeNode — класс с названием DefaultMutableTreeNode, который разрешает делать с узлом все, что только можно вообразить. Более того, он плотно «набит» различными полезными методами, которые не раз пригодятся при работе с деревом: эти методы позволят без труда определить, принадлежит ли некоторый узел вашему дереву, является ли он потомком или предком для другого узла, определить общего предка нескольких узлов, получить путь до корня дерева, работать с листьями, принадлежащими узлу, и делать еще многое с помощью нескольких простых вызовов. В дополнение к этому класс DefaultMutableTreeNode позволяет получать различные перечисления узлов и их потомков, в том числе и перечисления всего дерева по известным дисциплинам «в глубину» (depth first) и «в ширину» (breadth first). Благодаря всему этому класс DefaultMutableTreeNode может быть полезен и просто как универсальное средство описания узлов любой иерархической структуры данных (которое к тому же элементарно вывести на экран, как мы вскоре увидим).
Создать древовидную структуру с помощью класса DefaultMutableTreeNode очень просто. Для каждого узла своего дерева вы создаете отдельный объект этого класса, указывая в конструкторе данные, которые он будет хранить. Именно эти данные (а, точнее, результат вызова метода toString(), переданного вами в узел в качестве данных объекта) будут использоваться деревом для вывода узла на экран. Далее с помощью методов add() (для добавления узла к концу списка потомков другого узла) или insert() (для вставки узла на произвольную позицию) вы организуете иерархические отношения узлов. Начинается все с корня дерева, к которому вы добавляете первых потомков. К этим потомкам в свою очередь добавляются свои потомки, и так продолжается до построения всего дерева.
«Чудесно, но как отобразить эту структуру?» — спросите вы. Мы уже упоминали, что с узлами TreeNode работает стандартная модель дерева DefaultTreeModel. Эта мо-