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]));