Про java swing - часть 5

February 26, 2008

Деревья



Второй элемент управления активно используемый при разработке бизнес-приложений - это дерево (JTree). Мы используем деревья для отображения иерархической информации. В практике часто используется и сочетание JTree и JTable в одном элементе управления. Хотя в swing такой функциональности изначально не предусмотрено, однако на сайте sun в уроках по swing рассматривается достойный пример создания JTreeTable.

Рассмотрим как создать дерево: простейший способ это сделать - задать конструкторе JTree иерархию объектов (массивов). Каждый объект в таком массиве будет представлен как отдельный узел дерева:


  1. public static void main(String[] args) {                                         
  2.  
  3.     class Node {                                                                 
  4.         String name;                                                             
  5.  
  6.         public Node(String name) {                                               
  7.             this.name = name;                                                    
  8.         }                                                                        
  9.         // обратите внимание на 'перекрытый' метод toString                      
  10.         // именно это значение будет возвращено и отображено в метке узла дерева 
  11.         public String toString() {                                               
  12.             return "Node Item: " + name;                                         
  13.         }                                                                        
  14.     }                                                                            
  15.  
  16.     JFrame jf = new JFrame("Tree Demo 1");                                       
  17.     JTree jt = new JTree(new Object[]                                            
  18.             {"Apple", "Orange", "Grapes",                                        
  19.                     // вложенный массив элементов                                
  20.                     new Object[]{                                                
  21.                             "Spartak",                                           
  22.                             "Caesar",                                            
  23.                             "Mark"                                               
  24.                     },                                                           
  25.                     "Mikki Mouse", "Donald Duck", "Pluto",                       
  26.                     new Node("Joe")                                              
  27.             });                                                                  
  28.  
  29.     jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);                           
  30.     jf.getContentPane().add(new JScrollPane(jt));                                
  31.     jf.pack();                                                                   
  32.     jf.setVisible(true);                                                         
  33. }
Обратите внимание на следующий момент: Мы можем помещать внутрь дерева любой объект. Если для него переопределен метод toString, как для класса Node, то узел получает удобочитаемое представление. Если же это не так, например когда узлом является массив, то его представление неудобочитаемо, и вы вынуждены использовать рендереры. Стоит отметить, что использовать такой прием с заданием структуры в виде "очень вложенных массивов" хотя и просто, но фактически неприменимо для больших деревьев. Здесь нам приходится использовать модели данных: далее идет полный пример в котором строится дерево файловой системы:
  1. public class FileTree {
  2.     public static void main(String[] args) {
  3.         JFrame jf = new JFrame();
  4.         File[] froots = File.listRoots();
  5.         // получаем список 'корней' файловой системы.
  6.         // для windows - это имена дисков: a:, c:, d: e:
  7.         // для linux - /
  8.         DefaultMutableTreeNode super_root = new DefaultMutableTreeNode("My Computer");
  9.         // создаем узел являющийся корнем дерева
  10.         for (int i = 0; i < froots.length; i++) {
  11.             File froot = froots[i];
  12.             // для каждого корня файловой системы создается собственный узел дерева JTree
  13.             DefaultMutableTreeNode mroot = new DefaultMutableTreeNode(froot);
  14.             // затем он помещается внутрь корневого узла
  15.             super_root.add(mroot);
  16.             // и вызываем рекурсивную процедуру (да, да это так ужасно), 
  17.             // которая заполняет текущий узел (корень файловой системы)
  18.             // некоторым количеством вложенных узлов.
  19.             // глубина сканирования строго ограничена
  20.             addChildrensToNode(0, mroot, froot);
  21.         }
  22.  
  23.         // содаем графическое представление дерева
  24.         final JTree jt = new JTree(super_root);
  25.         // указываем для этого дерева Renderer
  26.         jt.setCellRenderer(
  27.                 new TreeCellRenderer() {
  28.                     // заготовка возвращаемого из renderer-а компонента
  29.                     JLabel jLabel = new JLabel("XXX");
  30.                     public final ImageIcon ICON_FILE = new ImageIcon("c:\\tmp\\file.png");
  31.                     public final ImageIcon ICON_DIR = new ImageIcon("c:\\tmp\\dir.png");
  32.                     public final ImageIcon ICON_MYCOMP = new ImageIcon("c:\\tmp\\comp.png");
  33.  
  34.                     public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
  35.                                                                   boolean expanded, boolean leaf, int row, boolean hasFocus) {
  36.                         // опять внимание на параметры метода:
  37.                         // узел дерева -  передается вовсе не пользовательская информация хранящаяся внутри узла,
  38.                         // а именно сам узел.
  39.                         // затем признаки того, выделен ли данный элемент, развернут ли узел, 
  40.                         // является ли данный узел конечным листом,
  41.                         // затем координаты узла (номер строки) и наконец получил ли данный элемент фокус ввода
  42.                         DefaultMutableTreeNode mtree = (DefaultMutableTreeNode) value;
  43.                         // в дальшейшем из полученного узла mtree извлекается и используется 
  44.                         // пользовательский объект с помощью getUserObject
  45.                         if (mtree.getUserObject() instanceof String) {
  46.                             // особая проверка необходимая для того, 
  47.                             // чтобы корректно обработать корневой элемент дерева (хранящий не файл и не каталог)
  48.                             // а, именно, текстовую надпись "My Computer"
  49.                             jLabel.setText(mtree.getUserObject().toString());
  50.                             jLabel.setIcon(ICON_MYCOMP);
  51.                             return jLabel;
  52.                         }
  53.                         File f = (File) mtree.getUserObject();
  54.                         // в противном случае, содержимое узла файл, то мы должны его соответствующим образом отобразить
  55.                         try {
  56.                             // в зависимости от того какое имя данного файла, мы возвращаем соответствующую надпись JLabel.
  57.                             // также изменяем и видимую иконку
  58.                             jLabel.setText(f.getCanonicalPath());
  59.                             jLabel.setIcon(f.isDirectory() ? ICON_DIR : ICON_FILE);
  60.                             return jLabel;
  61.                         } catch (IOException e) {
  62.                             e.printStackTrace();
  63.                         }
  64.                         return new JLabel("Ошибка");
  65.                     }
  66.                 }
  67.         );
  68.  
  69.         jt.setLargeModel(true);
  70.         jf.getContentPane().add(new JScrollPane(jt));
  71.  
  72.         JLabel lab_hint = new JLabel("Подсказка");
  73.         JScrollPane comp = new JScrollPane(lab_hint);
  74.  
  75.         jf.getContentPane().add(comp, BorderLayout.SOUTH);
  76.         jf.pack();
  77.         jf.setVisible(true);
  78.     }
  79.  
  80.     public static final int MAX_LEVELS = 2;
  81.     // данная переменная хранит значение максимального уровня для рекурсивного просмотра каталога
  82.  
  83.     private static void addChildrensToNode(int level, DefaultMutableTreeNode mroot, File froot) {
  84.         if (level >= MAX_LEVELS) return;
  85.         File[] fall = froot.listFiles();
  86.         if (fall == null) return;
  87.         for (int i = 0; i < fall.length; i++) {
  88.             File file = fall[i];
  89.             DefaultMutableTreeNode newChild = new DefaultMutableTreeNode(file);
  90.             mroot.add(newChild);
  91.             if (file.isDirectory())
  92.                 addChildrensToNode(level + 1, newChild, file);
  93.         }
  94.     }
  95. }


Для представления узла дерева наиболее удобно использовать класс DefaultMutableTreeNode. Этот класс содержит внутри себя пользовательский объект (информационную составляющую узла) и предоставляет список методов для управления: добавление, удаление ... для дочерних элементов. Также предоставляются простейшие средства для обхода деревьев деревьев (помните из курса СДА все эти восходящие и нисходящие, слева-направо и наоборот ...).
Метод Описание
void add(MutableTreeNode newChild) Добавить к узлу новый дочерний элемент. Элемент добавляется в конец списка дочерних узлов. Если же раньше данный элемент-узел был дочерним по отношению к чему-то еще, то старая связь разрывается.
Enumeration breadthFirstEnumeration() Возвращается интерфейс перечисления всех дочерних элементов в порядке обхода в ширину.
Enumeration children() Перечисление всех дочерних элементов (только непосредственно дочерние узлы).
Enumeration depthFirstEnumeration() перечисление всех дочерних элементов, но уже в порядке обхода в глубину.
boolean getAllowsChildren() метод возвращает признак того, может ли данный узел иметь дочерние элементы.
TreeNode getChildAt(int index) Поиск дочернего элемента по номеру.
int getChildCount() Возвращается количество дочерних элементов.
int getDepth() Возвращается глубина узла. Она расчитывается как наибольшая длина между текущим элементом и всеми его листами т.е. терминальными узлами.
TreeNode getFirstChild() Вернуть первый из дочерних элементов.
int getIndex(TreeNode aChild) Поиск по значению узла его порядкового номера.
TreeNode getLastChild() Вернуть последний узел среди дочерних.
int getLevel() Вернуть номер уровня (растояние от корня дерева до текущего элемента).
TreeNode getParent() Вернуть ссылку на родительский элемент.
TreeNode[] getPath() Возвращается массив узлов, через которые необходимо пройти, чтобы добраться до текущего узла.
TreeNode getRoot() Возвращается ссылка на корневой элемент для всего дерева.
int getSiblingCount() Возвращается количество сиблингов (“братских” или “сестерских” элементов для текущего узла).
Object getUserObject() Метод возвращает ссылку на пользовательский объект, хранящийся внутри узла.
Object[] getUserObjectPath() Метод похож на ранее рассмотренный getPath, но возвращается не массив узлов, а массив пользовательских объектов, хранящихся внутри узлов на пути от корня дерева до текущего элемента.
void insert(MutableTreeNode newChild, int childIndex) Добавить новый элемент как дочерний к текущему. Причем необходимо поместить элемент под указанным номером.
boolean isLeaf() Вернуть признак того, является ли данный узел терминальным (т.е. листом не имеющим дочерних узлов).
boolean isRoot() Вернуть признак того, что узел является корневым.
Enumeration preorderEnumeration() Снова возвращается список всех дочерних узлов для некоторого узла дерева, но обход дерева будет выполнен в порядке "обратный нисходящий".
void remove(int childIndex) Выполняется удаление одного из дочерних элементов под указанным номером.
void remove(MutableTreeNode aChild) Тоже что и предыдущий метод, но удаляем не по номеру, а по значению самого узла.
void removeAllChildren() Уничтожить все дочерние элементы.
void setParent(MutableTreeNode newParent) Переместить узел к другому родительскому узлу.
void setUserObject(Object userObject) Установить значение пользовательского объекта.

Выделение узлов дерева. Обработка событий "сворачивание" и "развертывание" узлов дерева



Для реации на то, как происходит выделение узлов и путей в дереве, используется концепция "модели выделения и слушателей событий выделения". Ниже приводится пример в развитие ранее приведенного (примера строящего дерево файловой системы). Ниже дерева добавляется надпись Jlabel, в которую выводится путь к выделенному элементу. А также пример демонстрирует как реагировать на сворачивание и развертывание узлов дерева, и то как мы можем разрешать или запрещать данную операцию.
  1. // теперь к дереву нужно добавить слушателей                                                            
  2. jt.addTreeExpansionListener(                                                                            
  3.         new TreeExpansionListener() {                                                                   
  4.             public void treeExpanded(TreeExpansionEvent event) {                                        
  5.                 // эти слушатели извещаются когда операция была завершена. Здесь раскрытие узла.        
  6.                 JOptionPane.showMessageDialog(jt, event.getPath().getLastPathComponent());              
  7.             }                                                                                           
  8.  
  9.             public void treeCollapsed(TreeExpansionEvent event) {                                       
  10.                 // эти слушатели извещаются когда операция была завершена. Здесь свертывание узла.      
  11.                 JOptionPane.showMessageDialog(jt, event.getPath().getLastPathComponent());              
  12.             }                                                                                           
  13.         }                                                                                               
  14. );                                                                                                      
  15.  
  16. jt.addTreeWillExpandListener(                                                                           
  17.         new TreeWillExpandListener() {                                                                  
  18.             public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {           
  19.                 // разрешено ли развернуть узел                                                         
  20.                 System.out.println ("treeWillExpand");                                                  
  21.                 if (Math.random() < 0.5)                                                                
  22.                     throw new ExpandVetoException(event, "увы, но развернуть узел дерева не возможно"); 
  23.             }                                                                                           
  24.  
  25.             public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {         
  26.                 // разрешено ли свернуть узел                                                           
  27.                 System.out.println("treeWillCollapse");                                                 
  28.             }                                                                                           
  29.         }                                                                                               
  30. );
Примечание: если мы хотим реагировать на событие "ДО СВОРАЧИВАНИЯ" или "ДО РАЗВЕРТЫВАНИЯ" узла дерева, то добавлем слушателя события для TreeWillExpandListener. В случае, когда мы хотим запретить операцию, то необходимо выбросить исключение ExpandVetoException из метода treeWillExpand или treeWillCollapse.

Как небольшое задание перепишите ранее приведенный пример построения файлового дерева, заменив рекурсивное сканирование файловой системы на "более умный подход". Вспомните, как мы изучая MFC делали похожий пример с деревом и CTree. Тогда дерево строилось только на один уровень вложенности, а недостающие узлы добавлялись динамически как только пользователь хотел развернуть некоторый узел, то мы обрабатывали событие "ДО РАЗВЕРТЫВАНИЯ" и достраивали дерево до нужного размера.

Categories: Java