Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Портянкин И. Swing

.pdf
Скачиваний:
210
Добавлен:
07.10.2020
Размер:
5 Мб
Скачать
☆

Уход за деревьями

455

}

// создаем стандартную модель return new DefaultTreeModel(root);

}

// этот слушатель следит за изменением выделения class SelectionL implements TreeSelectionListener { public void valueChanged(TreeSelectionEvent e) {

//получаем источник события — дерево

JTree tree = (JTree)e.getSource();

//добавленные/удаленные пути

TreePath[] paths = e.getPaths(); log.append("Изменено путей: " +

paths.length + "\n");

//выделенные элементы и их номера строк

TreePath[] selected = tree.getSelectionPaths(); int[] rows = tree.getSelectionRows();

//последние элементы в пути

for (int i=0; i<selected.length; i++) { log.append("Выделен: " +

selected[i].getLastPathComponent() + " на строке: " + rows[i] + "\n");

}

// полная информация о пути в дереве if ( selected.length > 0 ) {

TreePath path = selected[0]; Object[] nodes = path.getPath();

for (int i=0; i<nodes.length; i++) { // путь состоит из узлов

DefaultMutableTreeNode node = (DefaultMutableTreeNode)nodes[i];

log.append("Отрезок пути " + i + " : " + node.getUserObject() + " ");

}

}

log.append("\n");

}

}

public static void main(String[] args) { SwingUtilities.invokeLater(

new Runnable() {

public void run() { new TreeSelectionModes(); } });

456

ГЛАВА 15

}

}

Итак, в примере мы создаем окно, в котором разместится три дерева, каждое со стандартной моделью выделения, но с разным режимом работы этой модели. Для дерева JTree необходимо предоставить данные для отображения, чтобы было что выделять, так что в методе createTreeModel() мы создаем простую модель с корнем и двумя потомками, к которым присоединяются листья, хранящиеся для удобства в массивах. Здесь все просто, фактически повторяется пример для стандартной модели дерева, рассмотренный в предыдущем разделе. Полученную модель мы будем разделять между нашими видами — тремя деревьями, так что в них мы увидим одинаковые данные. В этом примере гораздо интереснее не данные, а то, как эти данные можно выделять. Создав на основе модели первое дерево, мы меняем режим выделения его узлов. Для этого нужно получить используемую деревом модель выделения (методом getSelectionModel()) и сменить режим выделения (методом setSelectionMode()). Три возможных режима выделения узлов дерева описаны константами интерфейса TreeSelectionModel, для первого дерева мы выбираем режим SINGLE_TREE_SELECTION, позволяющий выделить только один узел за один раз.

Точно так же мы поступаем и со вторым деревом, устанавливая для него режим CONTIGUOUS_TREE_SELECTION. Этот режим допускает выделение произвольного количества узлов дерева, но только с одним условием: выделяемые узлы дерева должны следовать один за другим без перерывов. Как только пользователь попытается выделить узел, не входящий в непрерывный интервал выделения, все выделение сбросится, и выбранным останется только один узел.

Никто не запрещает нам создавать стандартную модель выделения напрямую и передавать ее затем в метод setSelectionModel() класса JTree. Так мы и делаем для третьего дерева в примере, устанавливая для него режим выделения DISCONTIGUOUS_TREE_SELECTION. Кстати, мы могли этого и не делать, такой режим выделения является для стандартной модели режимом по умолчанию. Режим этот очень прост: пользователь может выделять любое количество узлов в любом порядке, как ему заблагорассудиться. К третьему дереву мы присоединяем слушателя событий от модели выделения TreeSelectionListener, который позволит нам быть постоянно в курсе того, что выделяет в дереве пользователь.

Уход за деревьями

457

Но о слушателе событий чуть позже, а пока нам остается только вывести наши деревья на экран и оценить различные режимы выделения. Для размещения трех деревьев используется панель с табличным расположением GridLayout, разделенная на три ячейки одинакового размера. В этих ячейках размещаются панели прокрутки с деревьями. Кроме того, на юг окна помещается многострочное текстовое поле JTextArea c названием log, в него мы будем записывать сообщения о событиях, поступающих слушателю событий от модели выделения. Запустив программу с примером и пробуя выделять различные узлы деревьев (дополнительные режимы выделения доступны при нажатых управляющих клавишах Ctrl и Shift), вы легко увидите разницу в режимах выделения и сможете выбрать для своего дерева тот режим, который вам больше всего подходит.

Ну а теперь о том, как с помощью слушателей TreeSelectionListener следить за всеми изменениями в выделенных элементах дерева JTree. Присоединить слушателя можно как к модели выделения, так и к самому дереву JTree. Это почти одно и то же, разница лишь в том, что именно будет возвращать метод getSource() в ответ на события. В первом случае он вернет нам ссылку на модель выделения, в которой произошло изменение, а во втором — ссылку на само дерево. В классе JTree дублируются все методы модели выделения (они просто делегируют ей свои вызовы), кроме того, в классе JTree есть несколько весьма полезных методов, способных упростить работу с выделенными элементами дерева, так что, как правило, лучше присоединять слушателя непосредственно к дереву. Мы так и поступаем. Наш слушатель описан во внутреннем классе SelectionL. Каждый раз при изменении в выделенных узлах дерева вызывается метод слушателя valueChanged(). Параметр этого метода — объект TreeSelectionEvent. Он позволяет получить, во-первых, как и в случае c любым событием, источник события (у нас это дерево JTree), во-вторых, список путей узлов, которые были затронуты при смене выделения (методом getPaths()). Обратите внимание, что этот метод не позволяет получить выделенные в данный момент узлы дерева, он всего лишь содержит список изменившихся в последний раз узлов. Например, если у вас в дереве уже выделено с десяток узлов, и пользователь решает прибавить к ним еще один, данный метод вернет только один путь для нового выделенного узла, так как остальные при этом не изменятся. Вы сможете убедиться в этом, запустив программу с примером и при выделении новых узлов отслеживая (в текстовом поле), сколько путей возвращает данный метод.

Гораздо удобнее узнавать обо всех выделенных в данный момент узлах с помощью метода getSelectionPaths(), который есть и в модели выделения, и в дереве (метод дерева просто обращается к модели выделения). Данный метод возвращает массив объектов-путей TreePath ко всем выделенным в данный момент узлам дерева. Как вы помните, мы уже отмечали, что путь TreePath — это список узлов, начиная с корня, по которым надо пройти для достижения определенного узла. В случае стандартной модели (как в нашем примере) путь состоит из списка объектов-узлов TreeNode. Как правило, весь путь к выделенному узлу требуется знать редко, чаще всего достаточно информации о самом выделенном узле. Для получения этой информации служит метод getLastPathComponent(), возвращающий последний узел в пути (это собственно и есть выбранный узел). Кроме того, вы можете работать со строками, а не с путями. Поскольку дерево на экране отображается последовательно (корень, потомки в порядке добавления, их потомки также в порядке добавления), можно подсчитать, на какой строке находится тот или иной узел3. Получить номера строк выделенных узлов позволяет метод getSelectionRows(). Однако у строк есть недостаток: при свертывании или развертывании узлов дерева их номера меняются, даже если не меняются выделенные элементы, и ваша информация устаревает. Все же удобнее пользоваться

3 Переводом узлов дерева в строки на экране занимается специальный объект, ассоциированный с моделью выделения и реализующий интерфейс RowMapper. Он работает за кулисами деревьев, так что подробно разбираться в нем необязательно.

458

ГЛАВА 15

путями TreePath, но номера строк могут быть достойной альтернативой. Вы также сможете увидеть их в текстовом поле log.

Ну и напоследок мы полностью исследуем путь TreePath первого выделенного элемента. Обратите внимание, что прежде чем получать пути, необходимо удостовериться, что массив, возвращаемый методом getSelectionPaths(), их вообще содержит. В то время как сам массив никогда не станет пустой ссылкой null, элементов в нем может и не быть, что необходимо учитывать (такое событие возникает, когда пользователь сбрасывает выделение в дереве). Анализировать путь проще всего с помощью метода getPath(), возвращающего массив узлов, начиная с корня, ведущих к нужному нам узлу. Так как мы используем стандартную модель и данные узлов у нас хранятся в объектах DefaultMutableTreeNode, мы можем смело преобразовывать узлы, из которых состоит путь, к данному типу. Данные всех узлов, составляющих путь, мы получаем методом getUserObject() и выводим в текстовое поле, так что вы сможете своими глазами убедиться в том, что путь начинается от корня и постепенно приводит нас к выделенному узлу.

Впечатляющая часть методов класса JTree посвящена именно работе с выделенными элементами, так что пользоваться можно не только интерфейсом модели выделения и методом getSelectionPaths(). Составим небольшую таблицу наиболее полезных методов класса JTree для работы с выделением, в случае необходимости вы сможете обращаться к ней как к простому справочнику (табл. 15.2).

Таблица 15.2. Методы класса JTree для работы с выделением

Методы

Описание

add(set)SelectionPath(s),

На самом деле данные методы принадлежат модели выделения

add(set)SelectionRow(s)

TreeSelectionModel дерева и позволяют программно прибавлять к вы-

 

деленным узлам новые. Но если вам захочется забыть о моделях,

 

Swing позволит это сделать, как будто их вовсе нет: все методы моде-

 

ли выделения есть и в классе самого дерева JTree

clearSelection()

Сбрасывает все выделенные в данный момент узлы дерева, так что

 

выделенного в нем ничего не остается

getMinSelectionRow(),

Удобные методы при работе с узлами дерева по номерам строк: пер-

getMaxSelectionRow()

вый метод возвращает номер первой выделенной строки (минималь-

 

ный), а второй номер последней выделенной строки (максимальный)

isPathSelected(),

Эта пара методов позволяет без труда выяснить, выделен ли некото-

isRowSelected()

рый узел в дереве (он может быть задан путем TreePath или номером

 

строки)

Внешний вид деревьев

Мы узнали, как подготавливать и настраивать модель данных дерева для вывода данных на экран и как работать с выделенными узлами. Теперь было бы неплохо познакомиться с тем, как дерево позволяет настраивать свой внешний вид и внешний вид своих узлов. Нетрудно догадаться, что все полностью находится в ваших руках, как и принято в Swing. Работу по отображению узлов дерево JTree выполняет не само, оно переадресует ее специальному отображающему объекту (renderer), который должен реализовывать несложный интерфейс TreeCellRenderer. Создавая необходимый отображающий объект и учитывая в нем особенности узлов вашего дерева, вы сможете расцветить его всеми возможными цветами и украсить любыми значками.

У вас также остаются общие унаследованные от базового класса JComponent библиотеки Swing свойства, такие как font или foreground, которые мы не раз уже обсуждали при рассмотрении различных компонентов Swing. Они позволят быстро сменить используе-

Уход за деревьями

459

мые деревом шрифты и цвета для всех узлов дерева разом. Это не самый гибкий подход, но иногда его оказывается достаточно.

Интерфейс отображающего объекта TreeCellRenderer на самом деле чрезвычайно прост. В нем определен всего один метод getTreeCellRendererComponent(), который обязуется возвратить некоторый графический компонент для переданных ему в качестве параметров дерева JTree, узла для отображения и дополнительных свойств узла (эти свойства определяют, выделен ли узел, раскрыт или свернут, лист ли это, владеет ли он фокусом ввода). Этот графический компонент и служит для отображения деревом узла. Используется данный компонент точно так же, как и в случае со списками JList, которые мы изучали в главе 11: он не добавляется на экран и остается незамеченным для графической системы, дерево задействует его как особую «кисть», вызывая метод paint() компонента в том месте, где располагается отображаемый в данный момент узел. Поэтому создавать новый компонент для каждого отображаемого узла не только не нужно, но и крайне расточительно и неэффективно. Компонент требуется один, для каждого узла просто меняются свойства этого компонента4.

По умолчанию в дереве JTree используется стандартный отображающий объект

DefaultTreeCellRenderer из пакета javax.swing.tree. Он унаследован от надписи JLabel и для каждого узла возвращает соответствующим образом настроенный (с подходящими текстом и значком) единственный экземпляр самого себя, то есть надписи. Этот экземпляр и служит для отображения всех узлов дерева. При этом объект DefaultTreeCellRenderer старается максимально оптимизировать прорисовку узлов дерева, в частности, он отключает все стандартные механизмы надписи JLabel, например, оповещение об изменении надписи и значка (это привязанные свойства JavaBeans) и автоматическую проверку корректности надписи. Учитывая, сколько узлов приходится отображать в больших деревьях, подобная оптимизация приходится весьма кстати.

Возникает резонный вопрос: «В дереве могут храниться данные любых типов, как их отображает стандартный объект?». Интересно, что для преобразования узла в строку, которая будет выведена на экран, стандартный отображающий объект не вызывает напрямую метод toString(), как это делает, к примеру, стандартный отображающий объект списка JList, а применяет особый метод класса JTree с названием convertValueToText(). По умолчанию данный метод просто вызывает для узла метод toString(), но его можно переопределить и по-своему обрабатывать преобразование узла в строку. Иногда это проще, чем написание нового отображающего объекта.

Объект DefaultTreeCellRenderer способен не только на отображение текста узла со стандартными значками. Как оказывается, его можно довольно тонко настроить, и во многих ситуациях этого оказывается достаточно, чтобы ваше дерево приняло именно тот внешний вид, который необходим приложению. При этом не стоит забывать, что стандартный отображающий объект унаследован от надписи JLabel, а, значит, вы смело можете использовать в тексте узла язык HTML, начиная его с «волшебной» комбинации символов <html>. Уже одно это дает вам прекрасные возможности для получения текста различных размеров, цветов и начертаний буквально одним мановением руки. Более того, легко можно сменить значки, необходимые для вывода узлов, и еще некоторые свойства (табл. 15.3).

Таблица 15.3. Свойства стандартного отображающего объекта дерева

Свойства

Описание

font

Задает шрифт, который будет использоваться отображающим

 

объектом для вывода узлов. Того же эффекта можно добиться

 

сменой свойства font самого дерева JTree

4 Это ярко выраженный шаблон проектирования приспособленец, упомянутый нами в главе 11, когда мы в первый раз столкнулись с отображающими объектами.

460

ГЛАВА 15

Таблица 15.3 (продолжение)

 

 

 

 

 

Свойства

Описание

 

 

 

closedIcon

Позволяет сменить значок, обозначающий свернутый узел

 

с потомками, например, в дереве с файлами это нераскрытая

 

папка

openIcon

Значок для раскрытого узла с потомками (в дереве файлов это

 

раскрытая папка)

leafIcon

Задает значок для узла, который является листом, то есть не

 

обладает потомками. Как вы помните из обсуждения стандарт-

 

ной модели, есть два способа определения того, какой узел

 

следует считать листом

backgroundNonSelectionColor,

Управляют цветом фона отображаемых на экране узлов. Пер-

backgroundSelectionColor

вое свойство задает цвет фона для узла, когда он не выделен,

 

второе — когда выделен

textNonSelectionColor,

Управляют цветом шрифта узлов. Первое свойство хранит

textSelectionColor

цвет шрифта невыделенного узла, второе — выделенного

 

Используя любые поддерживаемые Swing возможности HTML и перечисленные в таблице свойства, можно удивительным образом раскрасить любое дерево. Рассмотрим пример, в котором задействуем самые эффектные возможности стандартного отображающего объекта дерева JTree:

//TreeDefaultRendering.java

//Использование возможностей стандартного

//отображащего объекта DefaultTreeCellRenderer import javax.swing.*;

import javax.swing.tree.*; import java.awt.*;

public class TreeDefaultRendering extends JFrame { public TreeDefaultRendering() {

super("TreeDefaultRendering"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//создаем дерево на основе незатейливой модели

JTree tree = new JTree(createTreeModel());

//создадем и настраиваем отображающий объект

DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();

renderer.setLeafIcon(new ImageIcon("Leaf.gif")); renderer.setClosedIcon(

new ImageIcon("NodeClosed.gif")); renderer.setOpenIcon(

new ImageIcon("NodeOpened.gif"));

//передаем его дереву tree.setCellRenderer(renderer);

Уход за деревьями

461

// добавляем дерево и выводим окно на экран 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);

}

public static void main(String[] args) { SwingUtilities.invokeLater(

new Runnable() {

public void run() { new TreeDefaultRendering(); } });

}

}

Здесь мы создаем небольшое окно с деревом JTree. Дерево строится из стандартной модели (а та, в свою очередь, принимает данные от стандартных узлов DefaultMutableTreeNode). Нетрудно заметить, что создание модели дерева в методе createTreeModel() практически не отличается от предыдущего примера. Единственное отличие в том, что мы стали использовать для данных некоторых узлов не простой текст,

462

ГЛАВА 15

а HTML-разметку, не забыв предупредить об этом отображающий объект с помощью магических символов <html>.

Посмотрите, как одним мановением руки настраивается стандартный отображающий объект дерева DefaultTreeCellRenderer. Мы сменяем три значка, используемые для вывода узлов и листьев, не затрагивая цвета и шрифты (это, как нетрудно заметить, гораздо проще сделать с помощью встроенного языка HTML). Чаще всего именно сменой значков для узлов дерева добиваются его уникального узнаваемого вида, который к тому же в состоянии «подсказать» пользователю, данные какого типа дерево отображает. Значки, показанные в примере, были частично заимствованы из коллекции графики Java Look And Feel Graphics Repository, специально созданной для внешнего вида Metal (вы можете найти эту коллекцию по адресу java.sun.com).

ПослетогокакмызадаемнашновыйотображающийобъектметодомsetCellRenderer(), остается включить дерево JTree в панель прокрутки и поместить его в окно, после чего вывести окно на экран. Заметьте, насколько мало кода нам понадобилось написать (если не считать кода, создающего модель дерева) и какой совершенно уникальный облик обрело наше дерево. Обратите внимание, как показаны узлы, названия которых написаны с помощью HTML. Одним словом, фантазия ваша в отображении узлов дерева ничем не ограничена, нужно лишь следить за тем, чтобы узлы дерева имели более или менее одинаковую высоту, иначе дерево теряет стройность и начинает запутывать пользователя.

Дерево с флажками

Конечно, вам не обязательно использовать стандартный отображающий объект DefaultTreeCellRenderer, который при всех своих преимуществах все же довольно ограничен в возможностях: значки и стиль текста задаются для всего дерева сразу, вы не сможете задать разные значки для разных узлов, а это требуется не так уж и редко. Поэтому иногда приходится создавать собственные отображающие объекты. Сделать это несложно — нам уже известно, что в интерфейсе отображающего объекта TreeCellRenderer всего один метод, который должен вернуть для узла дерева компонент, призванный отображать данные узла на экране.

Создадим теперь специализированный компонент пользовательского интерфейса — дерево, способное отображать флажки. В некоторых ситуациях применение подобного компонента делает пользовательский интерфейс значительно удобнее (к примеру, там, где имеется некая иерархия множества доступных свойств приложения). Для создания

Уход за деревьями

463

такого дерева нам понадобится как-то хранить данные узлов (текст и признак выбора флажка) и написать отображающий объект для таких данных. В главе 11 мы написали класс CheckBoxListElement для списка, состоящего из флажков; используем его и здесь (данный класс хранит именно текст и признак выделения элемента). Чтобы новое дерево было удобно задействовать в ваших программах, мы разместим его в библиотечном пакете com.porty.swing. Отображающий объект будет сразу же присоединяться в конструкторе дерева:

//com/porty/swing/CheckBoxTree.java

//Дерево, способное отображать в качестве узлов флажки package com.porty.swing;

import java.awt.*; import java.awt.event.*; import javax.swing.*;

import javax.swing.tree.*;

public class CheckBoxTree extends JTree { // конструктор на основе модели

public CheckBoxTree(TreeModel model) { super(model);

//задаем собственный отображающий объект setCellRenderer(new CheckBoxRenderer());

//следим за щелчками мыши addMouseListener(new MouseL());

}

//стандартный объект для отображения узлов private DefaultTreeCellRenderer renderer =

new DefaultTreeCellRenderer();

//флажок для отображения узлов дерева class CheckBoxRenderer extends JCheckBox

implements TreeCellRenderer { public CheckBoxRenderer() {

// делаем флажок прозрачным setOpaque(false);

}

// данный метод должен вернуть компонент для узла public Component getTreeCellRendererComponent( JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row,

boolean hasFocus) {

// проверяем, что используется стандартная модель if (!(value instanceof DefaultMutableTreeNode)) {

464

ГЛАВА 15

// если нет, то используем стандартный объект return renderer.getTreeCellRendererComponent(

tree, value, selected, expanded, leaf, row, hasFocus);

}

Object data = ((DefaultMutableTreeNode)value). getUserObject();

// проверяем, подходит ли нам тип данных узла if ( data instanceof CheckBoxListElement ) {

CheckBoxListElement element = (CheckBoxListElement)data;

// настраиваем флажок setSelected(element.isSelected()); setText(element.getText()); return this;

}

// иначе задействуем стандартный объект

return renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

}

}

// класс, следящий за щелчками мыши class MouseL extends MouseAdapter {

public void mousePressed(MouseEvent e) { // получим путь к узлу

TreePath path = getClosestPathForLocation( e.getX(), e.getY());

if ( path == null ) return;

// проверим, подходят ли нам данные узла

Object _node = path.getLastPathComponent();

if (_node instanceof DefaultMutableTreeNode) { DefaultMutableTreeNode node =

(DefaultMutableTreeNode)_node; Object data = node.getUserObject();

if ( data instanceof CheckBoxListElement ) { // меняем состояние флажка

CheckBoxListElement element = (CheckBoxListElement)data;

element.setSelected(! element.isSelected()); repaint(getPathBounds(path));

}