Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 38 - Swing: Komponenten II |
Nach JTable ist JTree die zweite der "großen" Elementarkomponenten in Swing. Sie dient zur Darstellung, Navigation und Bearbeitung baumartiger, hierarchischer Datenstrukturen.
Ein einfaches Beispiel für derartige Baumstrukturen stellt etwa das Dateisystem unter UNIX oder Windows dar. Es besteht aus einer Wurzel (dem Root-Verzeichnis) und darin enthaltenen Unterverzeichnissen. Die Unterverzeichnisse können ihrerseits weitere Unterverzeichnisse enthalten usw. Dadurch entsteht eine beliebig tief geschachtelte Struktur von Verzeichnissen, die baumartig durchlaufen und bearbeitet werden kann. Andere Beispiele für Baumstrukturen sind die syntaktischen Elemente einer Programmiersprache, die Aufbauorganisation eines Unternehmens oder das Inhaltsverzeichnis in einem Buch.
Im Umgang mit Bäumen haben sich folgende Begriffe eingebürgert:
Der eigentliche Aufwand beim Erzeugen eines Baums liegt im Aufbau eines passenden Datenmodells, das seiner Struktur nach meist ebenfalls hierarchisch ist. Das Instanzieren des JTree ist dann vergleichsweise einfach. Die beiden wichtigsten Konstruktoren der Klasse JTree sind:
public JTree(TreeModel newModel) public JTree(TreeNode root) |
javax.swing.JTree |
Der erste von beiden erwartet ein vordefiniertes TreeModel zur Darstellung der Elemente des Baums. Ein TreeModel kapselt alle relevanten Informationen über die Struktur des Baums. Es liefert auf Anfrage dessen Wurzel, stellt Informationen über einen bestimmten Knoten zur Verfügung oder liefert dessen Unterknoten.
An den zweiten Konstruktor wird lediglich die Wurzel des Baums übergeben. Sie wird vom Konstruktor automatisch in ein geeignetes TreeModel eingebettet. Beide Varianten sind prinzipiell gleichwertig. Zwar erfragt der JTree die zur Darstellung und Navigation erforderlichen Daten immer beim TreeModel. Aber das mit der Baumwurzel des zweiten Konstruktors instanzierte DefaultTreeModel ist in der Lage, diese Informationen aus den Knoten und den darin gespeicherten Verweisen auf ihre Unterknoten zu entnehmen (alle nötigen Informationen werden in den TreeNodes selbst gehalten). Wir werden später auf beide Arten, Bäume zu konstruieren, noch genauer eingehen.
Ein JTree besitzt nicht so viele Konfigurationsoptionen wie eine JTable. Die wichtigste von ihnen regelt, ob die Wurzel des Baums bei seiner Darstellung angezeigt oder unterdrückt werden soll. Auf sie kann mit den Methoden setRootVisible und isRootVisible zugegriffen werden:
public void setRootVisible(boolean rootVisible) public boolean isRootVisible() |
javax.swing.JTree |
Wir wollen uns zunächst ein einfaches Beispiel ansehen. Das folgende Programm erzeugt eine rekursive Baumstruktur mit Wurzel und zwei Unterebenen, deren Knoten aus Objekten des Typs DefaultMutableTreeNode bestehen. Diese im Paket javax.swing.tree gelegene Klasse ist eine Standardimplementierung des TreeNode-Interfaces, das beschreibt, wie ein Knoten Informationen über seine Unter- und Vaterknoten zur Verfügung stellen kann. Die vier wichtigsten Methoden von TreeNode sind:
public int getChildCount() public TreeNode getChildAt(int childIndex) public TreeNode getParent() public boolean isLeaf() |
javax.swing.tree.TreeNode |
Mit getChildCount kann die Anzahl der Unterknoten ermittelt werden. Sie werden von 0 an durchnumeriert, getChildAt liefert einen beliebigen Unterknoten. Ein Knoten kennt seinen Vaterknoten, der mit getParent ermittelt werden kann. Mit isLeaf kann zudem abgefragt werden, ob ein Knoten ein Blatt ist oder weitere Unterknoten enthält. Zur Beschriftung des Knotens bei der visuellen Darstellung verwendet ein JTree die Methode toString der Knotenklasse.
Mit DefaultMutableTreeNode steht eine recht flexible Implementierung von TreeNode zur Verfügung, die auch Methoden zum Einfügen und Löschen von Knoten bietet (sie implementiert übrigens das aus TreeNode abgeleitete Interface MutableTreeNode):
public DefaultMutableTreeNode(Object userObject) public void add(MutableTreeNode newChild) public void insert(MutableTreeNode newChild, int childIndex) public void remove(int childIndex) public void removeAllChildren() public void setUserObject(Object userObject) public Object getUserObject() |
javax.swing.tree.DefaultMutableTreeNode |
Mit add wird ein neuer Kindknoten an das Ende der Liste der Unterknoten angefügt, mit insert kann dies an einer beliebigen Stelle erfolgen. remove entfernt einen beliebigen und removeAllChildren alle Kindknoten. Anwendungsbezogene Informationen werden in einem UserObject gehalten, das direkt an den Konstruktor übergeben werden kann. Mit setUserObject und getUserObject kann auch nach der Konstruktion noch darauf zugegriffen werden. Das UserObject ist auch der Lieferant für die Knotenbeschriftung: jeder Aufruf von toString wird an das UserObject weitergeleitet.
DefaultMutableTreeNode stellt noch weitaus mehr als die hier beschriebenen Methoden zur Verfügung. Die Klasse ist sehr vielseitig und kann auch unabhängig von der Verwendung in einem JTree zum Aufbau und zur Verarbeitung baumartiger Datenstrukturen verwendet werden. |
|
Das folgende Programm erzeugt eine Wurzel mit fünf Unterknoten, die jeweils drei weitere Unterknoten enthalten. Anschließend wird der Wurzelknoten an den Konstruktor eines JTree übergeben und dieser durch Einbetten in eine JScrollPane (um automatisches Scrollen zu ermöglichen) in einem JFrame plaziert.
001 /* Listing3811.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 import javax.swing.*; 006 import javax.swing.tree.*; 007 008 public class Listing3811 009 extends JFrame 010 { 011 public Listing3811() 012 { 013 super("JTree 1"); 014 addWindowListener(new WindowClosingAdapter()); 015 //Einfaches TreeModel bauen 016 DefaultMutableTreeNode root, child, subchild; 017 root = new DefaultMutableTreeNode("Root"); 018 for (int i = 1; i <= 5; ++i) { 019 String name = "Child-" + i; 020 child = new DefaultMutableTreeNode(name); 021 root.add(child); 022 for (int j = 1; j <= 3; ++j) { 023 subchild = new DefaultMutableTreeNode(name + "-" + j); 024 child.add(subchild); 025 } 026 } 027 //JTree erzeugen 028 JTree tree = new JTree(root); 029 tree.setRootVisible(true); 030 //JTree einfügen 031 Container cp = getContentPane(); 032 cp.add(new JScrollPane(tree), "Center"); 033 } 034 035 public static void main(String[] args) 036 { 037 Listing3811 frame = new Listing3811(); 038 frame.setLocation(100, 100); 039 frame.setSize(250, 200); 040 frame.setVisible(true); 041 } 042 } |
Listing3811.java |
Mit aufgeklapptem zweiten und vierten Knoten sieht das Programm wie in Abbildung 38.9 dargestellt aus. Auf der linken Seite wird der Baum im Metal-, auf der rechten im Windows-Look-and-Feel gezeigt.
Abbildung 38.9: Ein einfacher JTree im Metal- und Windows-Look-and-Feel
Das Selektieren von Knoten wird durch das TreeSelectionModel gesteuert, auf das mit Hilfe der Methoden setSelectionModel und getSelectionModel zugegriffen werden kann:
public void setSelectionModel(TreeSelectionModel selectionModel) public TreeSelectionModel getSelectionModel() |
javax.swing.JTree |
Standardmäßig erlaubt ein JTree das Selektieren mehrerer Knoten. Soll die Selektionsmöglichkeit auf einen einzelnen Knoten beschränkt werden, muß ein eigenes TreeSelectionModel an setSelectionModel übergeben werden. Dazu kann eine Instanz der Klasse DefaultTreeSelectionModel erzeugt und durch Aufruf von setSelectionMode und Übergabe einer der Konstanten SINGLE_TREE_SELECTION, CONTIGUOUS_TREE_SELECTION oder DISCONTIGUOUS_TREE_SELECTION konfiguriert werden:
public void setSelectionMode(int mode) |
javax.swing.tree.DefaultTreeSelectionModel |
JTree stellt eine Reihe von Methoden zur Verfügung, mit denen abgefragt werden kann, ob und welche Knoten selektiert sind. Die wichtigsten von ihnen sind:
public TreePath getSelectionPath() public TreePath[] getSelectionPaths() public TreePath getLeadSelectionPath() |
javax.swing.JTree |
Mit getSelectionPath wird das selektierte Element ermittelt. Bei aktivierter Mehrfachselektion liefert die Methode das erste aller selektierten Elemente. Ist kein Knoten selektiert, wird null zurückgegeben. getSelectionPaths gibt ein Array mit allen selektierten Knoten zurück. getLeadSelectionPath liefert das markierte Element.
Alle beschriebenen Methoden liefern Objekte des Typs TreePath. Diese Klasse beschreibt einen Knoten im Baum über den Pfad, der von der Wurzel aus beschritten werden muß, um zu dem Knoten zu gelangen. Mit getLastPathComponent kann das letzte Element dieses Pfads bestimmt werden. In unserem Fall ist das gerade der selektierte Knoten. Mit getPath kann der komplette Pfad ermittelt werden. An erster Stelle liegt dabei die Wurzel des Baums, an letzter Stelle das selektierte Element:
public Object getLastPathComponent() public Object[] getPath() |
javax.swing.tree.TreePath |
Soll ermittelt werden, ob und welche Elemente im Baum selektiert sind, können die Methoden isSelectionEmpty und isPathSelected aufgerufen werden:
public boolean isSelectionEmpty() public boolean isPathSelected(TreePath path) |
javax.swing.JTree |
Alternativ zum TreePath kann auf die selektierten Elemente auch mit Hilfe ihrer internen Zeilennummer zugriffen werden. Dazu besitzt jedes angezeigte Element im Baum eine fortlaufende Nummer, die mit 0 bei der Wurzel beginnt und sich dann zeilenweise bis zum letzten Element fortsetzt. Die zugehörigen Methoden heißen getSelectionRows und getLeadSelectionRow. Abhängig davon, wie viele Knoten oberhalb eines bestimmten Knotens sichtbar oder verdeckt sind, kann sich die Zeilennummer während der Lebensdauer des Baums durchaus verändern, und es gibt Methoden, um zwischen Knotenpfaden und Zeilennummern zu konvertieren. Wir wollen auf dieses Konzept nicht weiter eingehen. |
|
Dient der JTree zur Steuerung anderer Komponenten (etwa in explorerartigen Oberflächen), muß das Programm meist unmittelbar auf Änderungen der Selektion durch den Anwender reagieren. Dazu kann es einen TreeSelectionListener instanzieren und ihn mit addTreeSelectionListener beim JTree registrieren. Bei jeder Selektionsänderung wird dann die Methode valueChanged aufgerufen und bekommt ein TreeSelectionEvent als Argument übergeben:
public void valueChanged(TreeSelectionEvent event) |
javax.swing.event.TreeSelectionListener |
Dieses stellt unter anderem die Methoden getOldLeadSelectionPath und getNewLeadSelectionPath zur Verfügung, um auf den vorherigen oder aktuellen Selektionspfad zuzugreifen:
public TreePath getOldLeadSelectionPath() public TreePath getNewLeadSelectionPath() |
javax.swing.event.TreeSelectionEvent |
Das folgende Programm erweitert Listing 38.11 um die Fähigkeit, das selektierte Element auf der Konsole auszugeben. Dazu definiert es ein TreeSelectionModel für Einfachselektion und fügt einen TreeSelectionListener hinzu, der jede Selektionsänderung dokumentiert:
001 /* Listing3812.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 import javax.swing.*; 006 import javax.swing.event.*; 007 import javax.swing.tree.*; 008 009 public class Listing3812 010 extends JFrame 011 { 012 public Listing3812() 013 { 014 super("JTree 2"); 015 addWindowListener(new WindowClosingAdapter()); 016 //Einfaches TreeModel bauen 017 DefaultMutableTreeNode root, child, subchild; 018 root = new DefaultMutableTreeNode("Root"); 019 for (int i = 1; i <= 5; ++i) { 020 String name = "Child-" + i; 021 child = new DefaultMutableTreeNode(name); 022 root.add(child); 023 for (int j = 1; j <= 3; ++j) { 024 subchild = new DefaultMutableTreeNode(name + "-" + j); 025 child.add(subchild); 026 } 027 } 028 //JTree erzeugen und Einfachselektion aktivieren 029 JTree tree = new JTree(root); 030 TreeSelectionModel tsm = new DefaultTreeSelectionModel(); 031 tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 032 tree.setSelectionModel(tsm); 033 tree.setRootVisible(true); 034 //JTree einfügen 035 Container cp = getContentPane(); 036 cp.add(new JScrollPane(tree), "Center"); 037 //TreeSelectionListener hinzufügen 038 tree.addTreeSelectionListener( 039 new TreeSelectionListener() 040 { 041 public void valueChanged(TreeSelectionEvent event) 042 { 043 TreePath tp = event.getNewLeadSelectionPath(); 044 if (tp != null) { 045 System.out.println(" Selektiert: " + tp.toString()); 046 } else { 047 System.out.println(" Kein Element selektiert"); 048 } 049 } 050 } 051 ); 052 } 053 054 public static void main(String[] args) 055 { 056 try { 057 Listing3812 frame = new Listing3812(); 058 frame.setLocation(100, 100); 059 frame.setSize(250, 200); 060 frame.setVisible(true); 061 } catch (Exception e) { 062 } 063 } 064 } |
Listing3812.java |
Die Selektion kann auch programmgesteuert verändert werden:
public void clearSelection() public void addSelectionPath(TreePath path) public void addSelectionPaths(TreePath[] paths) public void setSelectionPath(TreePath path) public void setSelectionPaths(TreePath[] paths) |
javax.swing.JTree |
Mit clearSelection wird die Selektion vollständig gelöscht. Mit addSelectionPath und addSelectionPaths kann die Selektion um ein einzelnes oder eine Menge von Knoten erweitert werden. Mit setSelectionPath und setSelectionPaths werden - unabhängig von der bisherigen Selektion - die als Argument übergebenen Knoten selektiert.
Der Anwender kann die Knoten mit Maus- oder Tastaturkommandos öffnen oder schließen. Dadurch werden die Unterknoten entweder sichtbar oder versteckt. Das Programm kann diesen Zustand mit den Methoden isCollapsed und isExpanded abfragen:
public boolean isExpanded(TreePath path) public boolean isCollapsed(TreePath path) public boolean hasBeenExpanded(TreePath path) public boolean isVisible(TreePath path) public void makeVisible(TreePath path) public void expandPath(TreePath path) public void collapsePath(TreePath path) |
javax.swing.JTree |
isExpanded liefert true, wenn der Knoten geöffnet ist, isCollapsed, wenn er geschlossen ist. hasBeenExpanded gibt an, ob der Knoten überhaupt schon einmal geöffnet wurde. isVisible gibt genau dann true zurück, wenn der Knoten sichtbar ist, d.h. wenn alle seine Elternknoten geöffnet sind. Mit makeVisible kann ein Knoten sichtbar gemacht werden. Mit expandPath kann er geöffnet und mit collapsePath geschlossen werden.
Es ist ohne weiteres möglich, den Inhalt und die Struktur des Baums nach dem Anlegen des JTree zu ändern. Es können neue Knoten eingefügt, bestehende entfernt oder vorhandene modifiziert werden. Wird die Klasse DefaultMutableTreeNode als Knotenklasse verwendet, reicht es allerdings nicht aus, einfach die entsprechenden Methoden zum Ändern, Einfügen oder Löschen auf den betroffenen Knoten aufzurufen. In diesem Fall würde zwar die Änderung im Datenmodell durchgeführt werden, aber die Bildschirmdarstellung würde sich nicht verändern.
Änderungen im Baum müssen immer über das Modell ausgeführt werden, denn nur dort ist der JTree standardmäßig als TreeModelListener registriert und wird über Änderungen unterrichtet. Werden diese dagegen direkt auf den Knoten ausgeführt, bleiben sie dem Modell verborgen und die Anzeige wird inkonsistent.
Für einfache Änderungen reicht es aus, eine Instanz der Klasse DefaultTreeModel als TreeModel zu verwenden. Sie wird durch Übergabe des Wurzelknotens instanziert und stellt eine Vielzahl von Methoden zum Einfügen, Löschen und Ändern der Knoten zur Verfügung. Alle Änderungen werden durch Versenden eines TreeModelEvent automatisch an alle registrierten TreeModelListener weitergegeben und führen dort zu entsprechenden Aktualisierungen der Bildschirmdarstellung.
Die zum Ändern des Modells benötigten Methoden von DefaultTreeModel sind:
public void insertNodeInto( MutableTreeNode newChild, MutableTreeNode parent, int index ) public void removeNodeFromParent(MutableTreeNode node) public void nodeChanged(TreeNode node) public TreeNode[] getPathToRoot(TreeNode aNode) |
javax.swing.tree.DefaultTreeModel |
Mit insertNodeInto wird ein neuer Kindknoten an einer beliebigen Position zu einem Elternknoten hinzugefügt. Mit removeNodeFromParent wird ein beliebiger Knoten aus dem Baum entfernt (er darf auch Unterknoten enthalten), und nodeChanged sollte aufgerufen werden, wenn der Inhalt eines Knotens sich so geändert hat, daß seine Bildschirmdarstellung erneuert werden muß. getPathToRoot schließlich ist eine nützliche Hilfsmethode, mit der das zur Konstruktion eines TreePath-Objekts erforderliche Knoten-Array auf einfache Weise erstellt werden kann.
Das folgende Programm zeigt einen Baum, der zunächst nur den Wurzelknoten enthält. Dieser ist vom Typ DefaultMutableTreeNode und wird in ein explizit erzeugtes DefaultTreeModel eingebettet, daß an den Konstruktor des JTree übergeben wird. Neben dem JTree enthält das Programm drei Buttons, mit denen ein neuer Knoten eingefügt sowie ein vorhandener gelöscht oder seine Beschriftung geändert werden kann.
001 /* Listing3813.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 import javax.swing.*; 006 import javax.swing.event.*; 007 import javax.swing.tree.*; 008 009 public class Listing3813 010 extends JFrame 011 implements ActionListener 012 { 013 protected DefaultMutableTreeNode root; 014 protected DefaultTreeModel treeModel; 015 protected JTree tree; 016 017 public Listing3813() 018 { 019 super("JTree 3"); 020 addWindowListener(new WindowClosingAdapter()); 021 //JTree erzeugen und Einfachselektion aktivieren 022 root = new DefaultMutableTreeNode("Root"); 023 treeModel = new DefaultTreeModel(root); 024 tree = new JTree(treeModel); 025 TreeSelectionModel tsm = new DefaultTreeSelectionModel(); 026 tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 027 tree.setSelectionModel(tsm); 028 tree.setRootVisible(true); 029 //JTree einfügen 030 Container cp = getContentPane(); 031 cp.add(new JScrollPane(tree), BorderLayout.CENTER); 032 //ButtonPanel 033 JPanel panel = new JPanel(new FlowLayout()); 034 String[] buttons = new String[]{"AddChild", "Delete", "Change"}; 035 for (int i = 0; i < buttons.length; ++i) { 036 JButton button = new JButton(buttons[i]); 037 button.addActionListener(this); 038 panel.add(button); 039 } 040 cp.add(panel, BorderLayout.SOUTH); 041 } 042 043 public void actionPerformed(ActionEvent event) 044 { 045 String cmd = event.getActionCommand(); 046 TreePath tp = tree.getLeadSelectionPath(); 047 if (tp != null) { 048 DefaultMutableTreeNode node; 049 node = (DefaultMutableTreeNode)tp.getLastPathComponent(); 050 if (cmd.equals("AddChild")) { 051 DefaultMutableTreeNode child; 052 child = new DefaultMutableTreeNode("child"); 053 treeModel.insertNodeInto(child, node, node.getChildCount()); 054 TreeNode[] path = treeModel.getPathToRoot(node); 055 tree.expandPath(new TreePath(path)); 056 } else if (cmd.equals("Delete")) { 057 if (node != root) { 058 TreeNode parent = node.getParent(); 059 TreeNode[] path = treeModel.getPathToRoot(parent); 060 treeModel.removeNodeFromParent(node); 061 tree.setSelectionPath(new TreePath(path)); 062 } 063 } else if (cmd.equals("Change")) { 064 String name = node.toString(); 065 node.setUserObject(name + "C"); 066 treeModel.nodeChanged(node); 067 } 068 } 069 } 070 071 public static void main(String[] args) 072 { 073 try { 074 String plaf = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"; 075 UIManager.setLookAndFeel(plaf); 076 Listing3813 frame = new Listing3813(); 077 frame.setLocation(100, 100); 078 frame.setSize(300, 300); 079 frame.setVisible(true); 080 } catch (Exception e) { 081 } 082 } 083 } |
Listing3813.java |
Alle Button-Aktionen werden in actionPerformed ausgeführt. Darin wird zunächst das Action-Kommando abgefragt und dann in Zeile 046 der Pfad des selektierten Elements bestimmt. Ist dieser nicht leer, werden die Kommandos wie folgt ausgeführt:
Nach einigen Einfügungen, Änderungen und Löschungen sieht die Programmausgabe beispielsweise so aus:
Abbildung 38.10: Ein veränderbarer JTree
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |