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. Эта мо-