Портянкин И. Swing
.pdfУход за деревьями |
445 |
дель может использовать в качестве узлов любые объекты, реализующие интерфейс TreeNode, но нетрудно догадаться, что чаще всего ей передаются экземпляры класса DefaultMutableTreeNode. Стандартная модель и заботится о том, чтобы организованная нами структура вписывалась в рамки интерфейса TreeModel и выводилась деревом на экран.
Стандартная модель DefaultTreeModel
Итак, стандартная модель дерева DefaultTreeModel хранит иерархические структуры, образованные узлами TreeNode. Для создания стандартной модели достаточно передать
вее конструктор узел TreeNode, являющийся корнем дерева, а обо всем остальном модель позаботится сама, так что дерево, начинающееся с этого узла, будет правильно выведено на экран. Дополнительных возможностей в стандартной модели практически нет, и это неудивительно: все, что вам может понадобиться при работе с деревом, уже есть
вузлах DefaultMutableTreeNode (ну а если вы реализуете интерфейс TreeNode самостоятель-
но, то, скорее всего, на это есть веская причина, и нужные вам возможности у вас уже есть). Рассмотрим пример использования стандартной модели, который заодно станет первым примером применения узлов DefaultMutableTreeNode:
//UsingDefaultTreeModel.java
//Использование стандартной модели дерева и
//узлов DefaultMutableTreeNode
import javax.swing.*; import javax.swing.tree.*; import java.awt.*;
public class UsingDefaultTreeModel extends JFrame { // для удобства листья будем хранить в массивах
private String[] drinks = { "Коктейль", "Сок", "Морс" }; private String[] fruits = { "Яблоки", "Апельсины" }; public UsingDefaultTreeModel() {
super("UsingDefaultTreeModel"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем нашу древовидную структуру
DefaultMutableTreeNode root =
new DefaultMutableTreeNode("Корень дерева");
//основные ветви
DefaultMutableTreeNode drink =
new DefaultMutableTreeNode("Напитки"); DefaultMutableTreeNode fruit =
new DefaultMutableTreeNode("Фрукты");
//добавляем ветви root.add(drink); root.add(fruit);
//специальный конструктор
root.add(new DefaultMutableTreeNode("Десерт", true));
446 ГЛАВА 15
// добавляем листья
for (String _drink : drinks) { drink.add(
new DefaultMutableTreeNode(_drink, false));
}
for (String _fruit : fruits) { fruit.add(
new DefaultMutableTreeNode(_fruit, false));
}
//создаем стандартную модель и дерево
DefaultTreeModel dtm1 =
new DefaultTreeModel(root, true); JTree tree1 = new JTree(dtm1);
//модель можно создать, начиная с любого узла
DefaultTreeModel dtm2 = new DefaultTreeModel(drink); JTree tree2 = new JTree(dtm2);
//добавляем деревья в окно и показываем его setLayout(new GridLayout(1, 2));
add(new JScrollPane(tree1)); add(new JScrollPane(tree2)); setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingDefaultTreeModel(); } });
}
}
В примере мы создаем древовидную структуру на основе стандартных узлов DefaultMutableTreeNode так, как мы и обсуждали: сначала создается корень нашего дерева (объект root), к которому присоединяются его потомки (узлы с информацией о доступных нам напитках и фруктах, а также десерте). Далее мы присоединяем к этим узлам (первого уровня, корень является узлом нулевого уровня, кстати, уровень любого узла позволяет узнать полезный метод getLevel() класса DefaultMutableTreeNode) их потомков, которые для удобства хранятся в массивах и добавляются к дереву в цикле. Заметьте, что для узла с информацией о десерте мы использовали особенный конструктор: в качестве параметров он принимает не только данные, которые будет хранить узел, но еще и булево значение. Последнее управляет тем, как будет вести себя метод getAllowsChildren(), определенный в интерфейсе TreeNode. Если он возвращает true, это означает, что у данного узла могут быть потомки, даже если их в данный момент нет (такое поведение характерно, например, для пустого каталога без файлов, который, тем не менее, остается каталогом и в будущем может содержать приличное количество файлов). В стандартной модели DefaultTreeModel поддерживаются два способа, позволяющие различать листья дерева (узлы без потомков). Согласно первому способу считается, что узел является листом, если у него в дан-
Уход за деревьями |
447 |
ный момент нет потомков, а согласно второму узел не является листом, если его метод getAllowsChildren() возвращает true (то есть потомки могут появиться). Для включения второго способа нужно использовать специальный конструктор класса DefaultTreeModel, передав ему значение true в качестве второго параметра (что мы и сделали для первой модели в примере) или вызвав метод модели setAsksAllowsChildren(true). В этом случае узел «с десертом» будет выглядеть как «папка», так оно и должно быть, поскольку обычно предоставляется несколько видов десерта. Подобное поведение может пригодиться в программах довольно часто: в деревьях встречаются «ненастоящие» листья, в которых потомков нет, но они могут появиться, и их нужно отличать от листьев «настоящих».
Вторая модель дерева создается, начиная не с корня, а с узла drink, так что вы свободно можете показывать свое дерево по частям, если в этом возникнет необходимость. Во второй модели у нас нет пустых «папок», поэтому мы не используем второй параметр конструктора, и листами считаем все узлы, у которых нет потомков. Для того чтобы дерево начало отображать данные созданных нами стандартных моделей, достаточно передать их в соответствующий конструктор класса JTree (или вызвать метод setModel(), если вы предпочитаете другие конструкторы). После этого остается разместить деревья
вокне (мы разделяем окно на две равные части с помощью менеджера табличного расположения GridLayout и размещаем в них наши деревья, предварительно «обернутые»
впанели прокрутки JScrollPane) и вывести последнее на экран.
После того как вы насладитесь напитками и фруктами, представленными с помощью стандартной модели и дерева JTree, может возникнуть вопрос: а как у стандартной модели обстоит дело с динамическим изменением узлов и потомков? И здесь стандартная модель на высоте. У вас есть целый арсенал методов для динамической манипуляции деревом, начиная от полной смены корня (методом setRoot()), вставки (специальным методом insertNodeInto()) узла типа MutableTreeNode в узел такого же типа на произвольную позицию с автоматическим оповещением слушателей и заканчивая простым изменением созданных вами узлов DefaultMutableTreeNode, которые, как мы прекрасно знаем, очень легко изменять и обновлять.
Если вы меняете узлы, не касаясь модели, ей надо об этом сообщить, чтобы она, в свою очередь, сообщила дереву о необходимости перерисовки. Для этого можно воспользоваться методом reload() модели, сообщающим слушателям об изменении всей модели (впрочем, есть перегруженная версия этого метода, сообщающего об изменениях в определенном узле дерева), или обратиться к методам вида nodesXXX(), которых в стандартной модели несколько. Эти методы (например, метод nodesChanged()) позволяют со-
448 |
ГЛАВА 15 |
общать об изменениях в потомках некоторого узла и экономят время обновления дерева, сообщая только о действительно измененных узлах. «Точечное» обновление дерева очень важно для высокой производительности перерисовки и мгновенной реакции на действия пользователей, которые всегда в восторге от быстрых приложений.
Таблица 15.1. Методы стандартной модели для оповещения об обновлении данных
Методы |
Описание |
reload() |
Говорит модели о том, что полностью поменялся некоторый узел |
|
и его необходимо перезагрузить и перерисовать. Метод без параме- |
|
тров перезагружает все дерево начиная с корня. Однако если у вас |
|
не меняется структура узлов, лучше применять методы, описанные |
|
ниже, потому что после перезагрузки узла он будет полностью закрыт, |
|
так что пользователь потеряет из виду все узлы, которые он открыл |
|
вручную |
nodeStructureChanged() |
Полный аналог предыдущего метода с другим названием |
nodesWereInserted(), |
Говорит модели о том, что для указанного в качестве параметра узла- |
nodesWereRemoved() |
предка были добавлены или удалены узлы-потомки. Это более эф- |
|
фективные сообщения, чем два, описанные выше |
nodesChanged() |
Метод сообщает модели (а та дереву), что поменялось значение |
|
указанного узла, так что его необходимо перерисовать. Структура |
|
дерева при этом не меняется, что делает данный вызов самым эф- |
|
фективным. Именно его следует предпочитать при желании обновить |
|
внешний вид узла после обновления его данных. |
Для того чтобы убедиться в том, что «точечное» обновление модели позволяет получить высокую производительность даже если приходится постоянно обновлять изображение дерева на экране, напишем небольшой пример. В нем мы создадим очень большое, нагруженное узлами дерево, и заставим все узлы постоянно обновляться. Как правило, именно так ситуация и обстоит в приложениях, работающих с большими объемами данных. Посмотрим, что у нас получается:
//TreeModelUpdates.java
//Пример эффективного обновления большого
//дерева с переменными данными
import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath;
import javax.swing.*;
import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.*;
public class TreeModelUpdates extends JFrame { // дерево
private JTree tree;
// стандартная модель дерева private DefaultTreeModel model;
Уход за деревьями |
449 |
public TreeModelUpdates() { super("TreeModelUpdates"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создаем дерево на основе модели
tree = new JTree(model = createTreeModel()); add(new JScrollPane(tree));
setSize(400, 300); setVisible(true);
}
// создание несложной, но большой модели дерева private DefaultTreeModel createTreeModel() {
//корень нашего дерева
DefaultMutableTreeNode root = new DefaultMutableTreeNode(
"Большоооое Дерево");
//присоединяем листья
for (int i=0; i<100; i++) {
DefaultMutableTreeNode node = new ChangeableColorNode(); root.add(node);
for (int k=0; k<10; k++) { node.add(new ChangeableColorNode());
}
}
// создаем стандартную модель return new DefaultTreeModel(root);
}
// узел дерева, динамически обновляющий свое содержимое class ChangeableColorNode
extends DefaultMutableTreeNode implements ActionListener { // случайная задержка обновления
private int delay = (int) (Math.random()*3000 + 500); // цвета текста, хранимого в узле
private int r,g,b;
public ChangeableColorNode() {
// генерируем цвета и запускаем таймер обновления generateColors();
new Timer(delay, this).start();
}
// этот метод вызывается таймером
public void actionPerformed(ActionEvent e) { // обновляем цвета и текст
450 |
ГЛАВА 15 |
generateColors();
// обновляем узел если он развернут
TreePath path = new TreePath( model.getPathToRoot(this));
if ( tree.isVisible(path) ) { model.nodeChanged(this);
}
}
// метод генерирует случайные цвета и обновляет текст public void generateColors() {
r = (int) (Math.random()*255); g = (int) (Math.random()*255); b = (int) (Math.random()*255);
setUserObject("<html><font color=rgb(" +r+","+g+","+b+")>Какой-то цвет!");
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new TreeModelUpdates(); } });
}
}
В примере мы унаследуем от окна JFrame, в центр которого будет добавлена панель прокрутки с нашим деревом. Модель для дерева будет создана простым методом createTreeModel(), на основе уже хорошо нам знакомой стандартной модели дерева. Главным ее отличительным качеством является размер — к корню дерева присоединяется ни много ни мало 100 дочерних узлов, у которых, в свою очередь, будет по 10 узловпотомков. В качестве узлов применяется класс ChangeableColorNode, унаследованный от класса стандартного узла дерева DefaultMutableTreeNode.
Самое интересное происходим именно в классе этих специальных узлов. Прежде всего, строка, которая хранится в узле, представляет собой текст HTML определенного цвета, заданного с помощью трех стандартных цветов RGB. Цвета эти генерируются случайным образом в методе generateColors(), после чего текст (а точнее, его цвет) обновляется. Чтобы симулировать постоянное обновление узла, мы используем таймер, представленный классом Timer из пакета javax.swing. Мы можем беспрепятственно создавать множество объектов этого класса, так как таймер все равно работает из одного потока и не будет отнимать слишком много ресурсов. Для того чтобы создать таймер, необходимо указать ему период срабатывания в миллисекундах (мы генерируем его случайным образом, но делаем не меньше 500), а также слушателя ActionListener, которому таймер будет сообщать о срабатываниях. Слушателя реализует сам же класс нашего узла. Интересно, что таймер из пакета javax.swing, в отличии от стандартного таймера JDK из пакета java.util, всегда вызывает слушателя из потока рассылки событий, что очень удобно при работе со Swing.
Самое интересное происходит при срабатывании таймера и вызове метода actionPerformed(). Мы обновляем цвет текста узла, и дальше нам необходимо сообщить де-
Уход за деревьями |
451 |
реву о том, что узел поменялся и его нужно перерисовать. Здесь нам помогает и стандартная модель, и само дерево. С помощью стандартной модели и ее метода getPathToRoot() мы получаем путь до текущего узла ( в самом узле неизвестна вся иерархия его предков) и выясняем с помощью метода дерева isVisible(), виден ли этот путь в данный момент на экране. Если он виден, то мы используем метод стандартной модели nodeChanged(), чтобы сообщить дереву о перемене в узле. Конечно, дерево и само проверяет, видел ли узел на экране, прежде чем его рисовать, однако перед этим оно заново рассчитывает размеры узла и свои собственные. Проверив самостоятельно, виден ли узел на экране, мы делаем его обновление еще быстрее.
Запустив пример, вы сможете развернуть любые узлы и прокручивать дерево, но не заметите никаких проблем с производительностью. Точное обновление данных на экране всегда гарантирует нам прекрасную скорость приложения, написанного с применением Swing.
Оповещение в нестандартных моделях
Если ваши данные хранятся в иерархии и модель дерева TreeModel прекрасно им подходит, без сомнения стоит написать свою собственную модель на их основе. Однако останется вопрос оповещения дерева об изменениях в вашей модели, если последняя динамична и это подразумевает. Для этого в нестандартной модели придется поддерживать список слушателей и самостоятельно создавать и «рассылать» события TreeModelEvent, так как никакого базового класса с поддержкой слушателей и методами fireXXX() у нас нет (не считая стандартной модели, но она работает с узлами TreeNode). Подобный процесс мы подробно рассматривали в главе 3, посвященной событиям в Swing.
Однако, это довольно низкоуровневая работа, особенно это касается списков слушателей и создания событий со всеми нужными параметрами, которых у деревьев немало. К счастью, в библиотеке расширений SwingX имеется специальный класс с названием TreeModelSupport, который возьмет на себя заботу о списке слушателей, и предоставит
452 |
ГЛАВА 15 |
вам удобные методы fireXXX(), помогающие оповещать слушателей о переменах в ваших данных. Чтобы сократить повторяющийся код, всегда применяйте в своих нестандартных моделях этот класс, если включение библиотеки SwingX в ваше приложение не противоречит его архитектуре и не слишком накладно.
Модели деревьев — краткое резюме
В общем-то, описание модели дерева TreeModel на этом можно закончить. Мы узнали, что прямое использование модели для вывода на экран обычных деревьев не слишком оправдано — все-таки машинное представление иерархических данных довольно специфично. Впрочем, если ваше приложение работает с особыми древовидными структурами данных, например, хранит схему распределения в сети серверов имен, модель дерева может подойти как нельзя лучше и легко упорядочит ваши данные. Но чаще всего для создания деревьев применяются стандартная модель DefaultTreeModel и узлы DefaultMutableTreeNode, которые позволяют без трудностей манипулировать данными и быстро подготавливать их к выводу на экран. Как правило, данные дерева не записываются непосредственно в код (это неудобно и делает программу менее гибкой), а хранятся в файлах ресурсов в подходящем формате (например, в изначально являющимся иерархическим формате XML). При необходимости эти данные из файлов ресурсов считываются и включаются в узлы DefaultMutableTreeNode. Это можно сделать компактно, после чего полученные узлы передаются стандартной модели. И конечно у вас остаются все преимущества модели: данные отделяются от пользовательского интерфейса, а модель может быть разделена между несколькими видами.
Выделение
После появления вашего дерева на экране требуется определить, какие именно узлы или листья выбирает в нем пользователь, поскольку чаще всего именно с выбранной пользователем информацией приложению приходится работать. Информацию о выделенных узлах дерева хранит специальная модель выделения, описанная интерфейсом TreeSelectionModel (из пакета javax.swing.tree). Для выделения элементов дерева имеется не слишком много способов, здесь все довольно просто: выделение можно производить, вопервых, по одному узлу дерева, во-вторых, смежными интервалами по несколько узлов, в-третьих, произвольно, составляя выделение из любого набора узлов, входящих в дерево. Сложность состоит в другом: в дереве с его запутанной иерархической структурой узлы нельзя определить простым числовым индексом. Для определения местоположения узла в дереве используются два средства. Во-первых, это пути TreePath, состоящие из набора узлов, начиная с корня, переходя по которым можно достигнуть требуемого узла. Во-вторых, это номера строк в дереве, которые не слишком удобны, так как для одного и того же выделения номера могут быть разными (при раскрытии или свертывании узлов появляются новые строки и прежние номера строк становятся недействительными).
Таким образом, модель выделения TreeSelectionModel не слишком сложна. Как оказывается, гораздо сложнее потом работать с выделенными путями в дереве, по которым необходимо определять, какую информацию выбрал пользователь. Основные две группы методов модели выделения таковы: методы вида addSelectionPath(s) добавляют к уже имеющемуся выделению новые узлы, заданные путями TreePath, а методы вида setSelectionPath(s) заменяют текущее выделение новым, также заданным путями TreePath. Именно в этих методах и происходит вся работа. К примеру, если модель выделения работает в режиме выделения только одного узла за раз, при вызове метода addSelectionPath() она смотрит, есть ли в модели уже выделенные узлы, и, если таковые имеются, вместо добавления к ним нового узла выделение с узлов снимается, и вместо них выделенным становится новый узел. Если бы в модели поддерживался режим произвольного выделения, в данном методе новые узлы просто добавлялись бы к старым. То же относится
Уход за деревьями |
453 |
и к методам setSelectionPath(s). Последний выделенный в дереве путь отмечается моделью выделения особым образом и возвращается методом getLeadSelectionPath(). На работу дерева или режима выделения это не оказывает никакого влияния, хранить последний выделенный элемент требуют некоторые внешние виды Swing, которые для удобства пользователя оформляют его особым образом2.
Модель выделения дерева TreeSelectionModel поддерживает списки слушателей TreeSelectionListener, которые оповещаются при изменении выделенных элементов в дереве. Присоединив к используемой деревом модели выделения своего слушателя, вы будете оперативно узнавать о каждом изменении в выделенных элементах дерева и при необходимости сразу же на это реагировать.
Реализовывать модель выделения «с нуля» вам вряд ли понадобится, гораздо проще задействовать стандартную модель выделения DefaultTreeSelectionModel из пакета javax. swing.tree. Она поддерживает все три возможных режима выделения узлов дерева, списки слушателей, быстрый сброс выделенных в данный момент элементов и дает вам возможность в любой момент времени выяснить все о выделенных элементах. Именно стандартная модель (в режиме выделения произвольного количества узлов) используется по умолчанию деревом JTree. Рассмотрим небольшой пример и увидим, на что она способна:
//TreeSelectionModes.java
//Использование стандартной модели выделения и
//всех поддерживаемых ею режимов
import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import java.awt.*;
public class TreeSelectionModes extends JFrame { public TreeSelectionModes() {
super("TreeSelectionModes"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создадим модель нашего дерева
TreeModel model = createTreeModel();
//дерево с одиночным режимом выделения
JTree tree1 = new JTree(model); tree1.getSelectionModel().setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
//дерево с выделением непрерывными интервалами
JTree tree2 = new JTree(model); tree2.getSelectionModel().setSelectionMode(
TreeSelectionModel.CONTIGUOUS_TREE_SELECTION);
//модель выделения можно хранить и отдельно
TreeSelectionModel selModel =
2 Если пользователь на мгновение отвлечется от увлекательного процесса выделения узлов дерева, он затем без труда сможет определить, на каком месте остановился (благодаря специально оформленному последнему выделенному узлу).
454 |
ГЛАВА 15 |
new DefaultTreeSelectionModel(); selModel.setSelectionMode(
TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); JTree tree3 = new JTree(model); tree3.setSelectionModel(selModel);
//будем следить за выделением в последнем дереве tree3.addTreeSelectionListener(new SelectionL());
//размещаем деревья в панели
JPanel contents = new JPanel(new GridLayout(1, 3)); contents.add(new JScrollPane(tree1)); contents.add(new JScrollPane(tree2)); contents.add(new JScrollPane(tree3)); add(contents);
//добавляем текстовое поле add(new JScrollPane(log), "South");
//выводим окно на экран setSize(500, 300); setVisible(true);
}
// текстовое поле для информации
private JTextArea log = new JTextArea(5, 20); // листья дерева храним в массивах
private String[] langs = { "Java", "Scala", "Ruby" }; private String[] ides =
{"IDEA", "Eclipse", "NetBeans" };
//создание несложной модели дерева private TreeModel createTreeModel() {
//корень нашего дерева
DefaultMutableTreeNode root =
new DefaultMutableTreeNode("Создание кода");
//основные ветви
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]));
