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

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

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

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

465

}

}

}

}

Мы наследуем наше дерево от класса JTree и определяем только один конструктор на основе модели TreeModel. Как мы знаем, остальные конструкторы, создающие дерево с помощью простых структур данных, не слишком полезны, так что от их отсутствия мы потеряем немного. В конструкторе сразу же происходит дополнительная настройка дерева: мы задаем для него особый отображающий объект, который будет ответственен за вывод на экран флажков, а также присоединяем к нему слушателя событий от мыши MouseListener. Слушатель событий будет следить за нажатиями кнопок мыши и менять состояние флажков: мы уже знаем, что компоненты, служащие для отображения узлов дерева, не получают никаких событий (так как фактически отсутствуют на экране), поэтому их приходится поддерживать самостоятельно.

Отображающий объект для нового дерева унаследован от флажка JCheckBox (так будет проще настраивать свойства флажка) и реализует необходимый интерфейс

TreeCellRenderer. Метод getTreeCellRendererComponent(), определенный в данном интерфейсе, должен вернуть для некоторого узла дерева (известны само дерево, отображаемый узел и его состояние, в том числе, выбран ли он, является ли листом и т. п. — все это передается в качестве параметров) компонент, который данный узел отобразит. Так же как и в случае со списками и их отображающими объектами, компонент, как правило, один, он настраивается для каждого отображаемого элемента. Обратите внимание, что в конструкторе мы меняем свойство непрозрачности, устанавливая его в false. Флажки JCheckBox по умолчанию непрозрачны, а в составе других компонентов непрозрачные компоненты выглядят не слишком привлекательно, закрашивая все вокруг себя цветом фона. Для начала в методе getTreeCellRendererComponent() мы проверяем, что деревом используется стандартная модель и узлы представлены классом DefaultMutableTreeNode. Если это не так, работа передается стандартному отображающему объекту DefaultTreeCellRenderer, который специально для подобных случаев хранится в нашем дереве в виде переменной. Благодаря стандартному объекту наше новое дерево будет способно отображать не только флажки, но и любые другие элементы.

Если в дереве используется стандартная модель, то мы преобразуем узел к соответствующему типу и получаем значение, которое хранится в узле. Если значением является объектом CheckBoxListElement, в котором хранятся название узла и признак его выделения, в действие вступает наш отображающий объект. Он настраивает свойства флажка (флажком является сам отображающий объект), а именно текст и выделение, и возвращает флажок в качестве объекта, отображающего данный узел. Когда в узле хранится не объект CheckBoxListElement, а что-то иное, в действие снова вступает стандартный отображающий объект, который способен отобразить любой объект в виде строки. Обратите внимание, что мы не поддерживаем выделение цветом или рамку, когда флажок обладает фокусом или выбран. В этом примере мы будем считать, что главное — отобразить само состояние флажка, ну а добавить все вышеперечисленное очень просто, в том случае, если эти свойства вам понадобятся, надо будет лишь задать нужный цвет и рамку для флажка5.

5 Интересно, что найти цвет для выделенного узла дерева не так-то просто — в дереве JTree нет метода для его определения. Вы можете задействовать для этой цели стандартный отображающий объект, получив от него компонент с нужными настройками и получив его цвет фона методом getBackground().

466

ГЛАВА 15

Отображающий объект отвечает за вывод на экран флажков, слушатель мыши, присоединенный к дереву, обеспечит смену состояния флажков. Компонент, используемый для отображения узлов, в контейнер не добавляется, так что все необходимые ему события необходимо поддерживать вручную. В слушателе мы следим за нажатиями кнопок мыши. Прежде всего необходимо выяснить, в районе какого узла произошло нажатие кнопки. В этом нам помогает метод getClosestPathForLocation(), он возвращает путь TreePath до нужного нам узла. Данный метод может возвращать и пустую (null) ссылку (так может случиться, если в дереве не будет данных), так что нам необходимо проверить это. Если проверка успешна, мы получаем последний элемент пути — как нам уже известно, это тот узел, в районе которого и произошло событие. Подразумевая, что дерево использует стандартную модель, мы преобразуем узел к объекту DefaultMutableTreeNode и получаем хранящиеся в этом объекте данные (проверив предварительно тип узла). Если это данные нужного нам типа (CheckBoxListElement), то состояние флажка меняется на противоположное, после чего дерево перерисовывается. Обратите внимание на метод перерисовки: мы перерисовываем лишь нужный нам узел, а получить его прямоугольник на экране помогает метод дерева под названием getPathBounds(). В небольших деревьях «точечная» перерисовка вряд ли будет заметна, но в больших «лесах» с тысячами листьев вы обязательно оцените производительность.

Проверим теперь новое дерево в работе и попробуем отобразить несложную модель, в которой будет совмещены и флажки, и обычные элементы. Приведем пример:

//TestCheckBoxTree.java

//Проверка дерева с поддержкой флажков import com.porty.swing.*;

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

public class TestCheckBoxTree extends JFrame { public TestCheckBoxTree() {

super("TestCheckBoxTree"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//создаем модель и дерево

TreeModel model = createTreeModel(); CheckBoxTree tree = new CheckBoxTree(model);

//добавляем дерево в окно

add(new JScrollPane(tree)); // выводим окно на экран setSize(400, 300); setVisible(true);

}

// листья дерева храним в массивах

private String[] langs = { "Java", "Scala", "Ruby" }; private String[] ides =

{ "IDEA", "Eclipse", "NetBeans" };

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

467

// создание несложной модели дерева 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(

new CheckBoxListElement(false, langs[i]))); ide.add(new DefaultMutableTreeNode(

new CheckBoxListElement(false, ides[i])));

}

//создаем стандартную модель

return new DefaultTreeModel(root);

}

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

new Runnable() {

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

}

}

Основную часть программы примера занимает код создания модели (в методе createTreeModel()), которую затем попытается отобразить наше новое дерево с поддержкой флажков. Модель основана на тех же данных, что мы использовали для нескольких предыдущих примеров, только на этот раз данные листьев будут храниться в объектах CheckBoxListElement, так что дерево отобразит их в виде флажков. В модели будет две ветви (они созданы на основе обычных строк), у каждой имеется по три потомка (все эти потомки будут флажками). Для простоты и компактности примера потомки добавляются в цикле. Для хранения данных применяются объекты DefaultMutableTreeNode, которые затем передаются в стандартную модель

DefaultTreeModel.

Остальная часть программы примера и вовсе проста: мы создаем небольшое окно с рамкой, в центр его добавляется новое дерево CheckBoxTree, предварительно вставленное в панель прокрутки JScrollPane. Дерево создается на основе подготовленной модели. После создания модели и дерева остается лишь вывести окно на экран.

468

ГЛАВА 15

Запустив программу с примером, вы оцените внешний вид и удобство дерева с флажками, а заодно увидите, как флажки меняют свое состояние при нажатиях кнопок мыши6. Конечно, не хватает поддержки смены состояния посредством клавиатуры, но это легко исправить: просто присоедините к дереву CheckBoxTree слушателя событий KeyListener или, что еще лучше, потому что обеспечит дополнительную гибкость, зарегистрируйте

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

Подобным образом поддерживается отображение в дереве самых экзотичных данных. Дерево само заботится о том, чтобы они находились в иерархических отношениях, а вы можете хранить их в модели, стандартной или своей собственной, и правильно отображать. Все это обеспечит удивительную гибкость и прозрачность вашего кода, легкость проектирования и позволит максимально удовлетворить пользователя. Кстати,

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

Редактирование узлов

Узлы, а точнее хранящиеся в них данные, можно не только отображать, но и редактировать. По умолчанию редактирование узлов дерева отключено, но вы всегда можете включить его, присвоив свойству editable значение true. Редактированием узлов, так же как и их отображением, дерево занимается не само, оно поручает эту работу специальному редактору. Для того чтобы некий объект смог стать редактором узлов дерева JTree, ему необходимо реализовать интерфейс TreeCellEditor.

Интерфейс TreeCellEditor унаследован от базового интерфейса CellEditor, который описывает обязанности любого редактора сложных компонентов Swing, а именно — редактора деревьев и таблиц. В интерфейсе CellEditor представлено довольно много методов, предназначенных для выполнения различных вспомогательных функций: для отмены и прекращения редактирования; для регистрации и отсоединения слушателей, получа-

6 Интересно, что наше дерево при использовании мыши даже удобнее стандартного. В стандартной реализации для выбора узла необходимо щелкнуть на самом тексте, который может быть совсем невелик, в то время как метод getClosestPathForLocation() возвращает узел на одном уровне с щелчком даже если он находится от него на почтительном расстоянии. Неплохая добавка к интерфейсу, если вы чтите закон Фитса — чем больше объект на экране, тем проще в него попасть мышью.

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

469

ющих уведомления о процессе редактирования; для определения возможности редактирования ячейки в случае произошедшего события и т. п. В интерфейсе TreeCellEditor появляется еще один метод с названием getTreeCellEditorComponent(). Данный метод обязуется возвратить компонент, который и будет использован для редактирования данных узла. В итоге задача написания редактора для дерева «с нуля» оказывается довольно непростой: работы предстоит немало.

Однако, как и всегда в Swing, у вас есть возможность воспользоваться стандартными редакторами, поставляющимися вместе с библиотекой. Зачастую их возможностей вполне хватает для вашего приложения, а настраивать их и затем работать с ними совсем просто. Стандартный редактор для деревьев (кстати, тот же редактор применяется и для таблиц) реализован в классе DefaultCellEditor. Он позволяет организовать редактирование значений с помощью одного из трех компонентов: текстового поля JTextField, раскрывающегося списка JComboBox или флажка JCheckBox. Действие происходит следующим образом: вы создаете компонент для редактирования, настраиваете его (например, заполняете список элементами) и передаете в соответствующий конструктор стандартного редактора. Далее остается включить редактирование дерева и присоединить редактор методом setCellEditor(). Дерево JTree будет готово использовать настроенный вами стандартный редактор.

В пакете javax.swing.tree есть вспомогательный редактор DefaultTreeCellEditor. Он, хотя и реализует все предписанные интерфейсом TreeCellEditor методы, сам ничего не редактирует, а передает свои обязанности «настоящему» редактору, который вы можете задать в конструкторе. В качестве «настоящего» редактора, как правило, применяется только что описанный нами редактор DefaultCellEditor. Главным его достоинством является то, что он производит редактирование узла дерева, не убирая с экрана значок, ассоциированный с этим узлом. Такое поведение более понятно пользователю, чем исчезновение значка при редактировании и восстановление его на экране после окончания редактирования.

В следующем несложном примере попробуем применить стандартный редактор для дерева:

//TreeDefaultEditing.java

//Стандартные редакторы для деревьев import javax.swing.*;

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

public class TreeDefaultEditing extends JFrame { // листья дерева храним в массивах

private String[] basics = { "Красный", "Зеленый", "Синий" };

private String[] extendeds = { "Желтый", "Голубой", "Розовый" };

public TreeDefaultEditing() { super("TreeDefaultEditing"); setDefaultCloseOperation(EXIT_ON_CLOSE);

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

JTree tree = new JTree(createTreeModel());

//включаем редактирование

470

ГЛАВА 15

tree.setEditable(true);

//"настоящий" редактор

JComboBox combo = new JComboBox(

new String[] {"Красный", "Зеленый"}); DefaultCellEditor editor = new DefaultCellEditor(combo);

//специальный редактор для дерева

DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); DefaultTreeCellEditor treeEditor =

new DefaultTreeCellEditor(tree, renderer, editor);

//присоединяем редактор к дереву tree.setCellEditor(treeEditor);

//выводим окно на экран

add(new JScrollPane(tree)); setSize(400, 300); setVisible(true);

}

// создание несложной модели дерева private TreeModel createTreeModel() {

//корень нашего дерева

DefaultMutableTreeNode root =

new DefaultMutableTreeNode("Цвета");

//основные ветви

DefaultMutableTreeNode basic =

new DefaultMutableTreeNode("Основные"); DefaultMutableTreeNode extended =

new DefaultMutableTreeNode("Дополнительные"); root.add(basic);

root.add(extended);

// присоединяем листья

for (int i=0; i<basics.length; i++) { basic.add(new DefaultMutableTreeNode(basics[i])); extended.add(

new DefaultMutableTreeNode(extendeds[i]));

}

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

}

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

new Runnable() {

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

471

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

}

}

В примере мы создаем дерево JTree, основанное на простой модели, описывающей взаимоотношения цветов. Модель создается в методе createTreeModel(). Процесс наполнения модели данными нам уже прекрасно знаком.

Гораздо интереснее посмотреть, как настраиваются стандартные редакторы для нашего дерева. Прежде всего необходимо создать «настоящий» редактор DefaultCellEditor, тот самый, который можно затем будет передать в редактор DefaultTreeCellEditor. В качестве компонента, заведующего процессом редактирования, мы выбрали раскрывающийся список JComboBox, который создали на основе массива из двух строк. Настроенный список затем надо передать в конструктор класса DefaultCellEditor. После этого уже можно приступать ко второму редактору DefaultTreeCellEditor, он позволит нам проводить редактирование ячеек, не отказываясь от значков. Конструктор второго редактора требует сразу три параметра: дерево, стандартный отображающий объект и редактор для дерева. В дереве будет проводиться редактирование, стандартный отображающий объект7 потребуется для получения значков, ассоциированных с узлами, а редактор, естественно, будет выполнять редактирование. Настроенный редактор DefaultTreeCellEditor можно присоединять к дереву.

Запустив программу с примером, вы увидите, как стандартные редакторы позволяют организовать редактирование на основе раскрывающегося списка. Начать редактирование позволяет тройной щелчок мыши или двойной щелчок, выполненный с паузой. Кстати, с помощью специальных методов стандартных редакторов вы можете задать число щелчков мыши, необходимых для начала редактирования. Обратите внимание, что во время редактирования значок узла остается на экране — это «заслуга» редактора DefaultTreeCellEditor, используй мы сразу редактор DefaultCellEditor, значок во время редактирования с экрана исчезал бы. Помимо списка JComboBox вы также можете задействовать для редактирования текстовое поле JTextField или флажок JCheckBox, хотя последний применяется редко8. Однако редактирование с помощью стандартных редакторов все же довольно ограничено:

7 Редактор DefaultTreeCellEditor может работать только со стандартным отображающим объектом, что, конечно же, ограничивает область его применения.

8 Лучше обратиться к созданному нами недавно специальному дереву с флажками, позволяющему менять их состояние.

472

ГЛАВА 15

редактировать можно все узлы «без разбора», и для редактирования всех узлов всегда применяется один и тот же компонент. Для деревьев, отображающих сложные данные различных типов, зачастую приходится писать собственные редакторы.

Создание собственного редактора

Как мы уже упоминали, создание собственного редактора для узлов дерева может вылиться в приличную работу: методов в интерфейсе TreeCellEditor (большая часть которых находится в его базовом интерфейсе CellEditor) немало. Однако библиотека предлагает нам помощника — абстрактный класс AbstractCellEditor реализует большую часть методов интерфейса CellEditor, и в том числе предоставляет нам поддержку слушателей CellEditorListener. Эти слушатели оповещаются при окончании или отмене редактирования, чтобы компонент (дерево) знал, когда работа редактора оканчивается. Для того чтобы наш собственный редактор для дерева смог заработать, нужно будет определить всего два метода: метод getCellEditorValue() должен возвращать значение, в данный момент находящееся в редакторе, а уже знакомый нам метод getTreeCellEditorComponent() — компонент, который и задействуется деревом как редактор. Остальные методы реализованы классом AbstractCellEditor и их требуется переопределять только в том случае, если вам понадобятся дополнительные возможности.

Попробуем написать особый редактор для дерева, которое отображает каталог телефонных номеров некоторой компании. Телефонные номера могут быть отсортированы по отделам и подразделениями, которые, как правило, находятся в иерархических отношениях, так что использование дерева вполне оправдано. Редактировать номера, отображаемые деревом, с помощью стандартных редакторов неудобно: во-первых, они не обеспечивают никаких ограничений на вводимые пользователем данные (а вводить можно будет только телефонные номера), во-вторых, они позволяют редактировать все узлы даже со вспомогательной информацией.

Новый редактор для дерева с телефонами использует для редактирования текстовое поле JFormattedTextField, позволяющее вводить данные в определенном формате. Формат вводимых данных определяется специальной текстовой маской9, которая в нашем примере задает простой телефонный номер. Кроме того, редактор следит за тем, чтобы редактировать можно было только листья дерева, в которых и хранятся телефонные номера, а не остальные узлы, служащие для упорядочения информации.

//CustomTreeEditor.java

//Создание специализированного редактора узлов import javax.swing.*;

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

import java.awt.event.*; import java.util.EventObject;

public class CustomTreeEditor extends JFrame { public CustomTreeEditor() {

super("CustomTreeEditor"); setDefaultCloseOperation(EXIT_ON_CLOSE);

9 Мы обсудим текстовое поле JFormattedTextField и поддерживаемые им форматы данных в главе 16, которая полностью посвящена текстовым компонентам Swing.

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

473

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

JTree tree = new JTree(createTreeModel());

//включаем редактирование узлов tree.setEditable(true); DefaultTreeCellRenderer renderer =

new DefaultTreeCellRenderer(); DefaultTreeCellEditor editor =

new DefaultTreeCellEditor(tree, renderer, new MaskTreeEditor(tree));

tree.setCellEditor(editor);

//выводим окно на экран

add(new JScrollPane(tree)); setSize(400, 300); setVisible(true);

}

// список телефонов

private String[] phoneDirectory = { "123-13-13", "444-55-67", "111-23-45"};

// создает модель дерева

private TreeModel createTreeModel() { DefaultMutableTreeNode root =

new DefaultMutableTreeNode("Компания"); DefaultMutableTreeNode node =

new DefaultMutableTreeNode("Отдел кадров"); root.add(node);

// присоединяем листья

for (String phone : phoneDirectory) { node.add(new DefaultMutableTreeNode(phone));

}

return new DefaultTreeModel(root);

}

// специальный редактор узлов дерева

class MaskTreeEditor extends AbstractCellEditor implements TreeCellEditor {

// дерево

private JTree tree;

//текстовое поле, применяемое для редактирования private JFormattedTextField editor;

//конструктор редактора

public MaskTreeEditor(JTree tree) { this.tree = tree;

// создаем форматирующий объект

474 ГЛАВА 15

try {

MaskFormatter phone =

new MaskFormatter("###-##-##"); editor = new JFormattedTextField(phone);

} catch (Exception ex) { ex.printStackTrace();

}

// присоединяем к полю слушателя editor.addActionListener(new ActionListener() {

// вызывается при окончании редактирования public void actionPerformed(ActionEvent e) {

stopCellEditing();

}

});

}

// возвращает компонент, используемый как редактор public Component getTreeCellEditorComponent(

JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row) {

//устанавливаем новое значение editor.setText(value.toString());

//возвращаем текстовое поле return editor;

}

//возращает текущее значение в редакторе public Object getCellEditorValue() {

return editor.getText();

}

//определяет, можно ли проводить редактирование public boolean isCellEditable(EventObject event) {

MutableTreeNode node = (MutableTreeNode) tree.getLastSelectedPathComponent();

return ( (node != null) && ( node.isLeaf()) );

}

}

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

new Runnable() {

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

}

}