JTree (Tutorial)©: Unterschied zwischen den Versionen

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen
K
K
Zeile 622: Zeile 622:
  
  
 +
Die getTreeCellRendererComponent-Methode hat eine ganze Reihe von Argumenten. Für jedes eine kurze Beschreibung.
 +
 +
'''JTree tree''' Das ist die JTree-Instanz, für die der Renderer etwas zeichnen soll (der Renderer kann von mehreren JTrees gleichzeitig benutzt werden).<br>
 +
'''Object value''' Das ist der Knoten, der gezeichnet werden soll. Dieses Objekt stammen vom TreeModel und wurden über die Methoden getChild() oder getRoot() abgerufen.<br>
 +
'''boolean selected''' Gibt an, ob der Knoten vom Benutzer selektiert wurde. Üblicherweise wird dann ein anderer Hintergrund verwendet.<br>
 +
'''boolean expanded''' Gibt an, ob der aktuelle Knoten geöffnet wurde. Bei einem geöffneten Knoten sind all seine Kinder sichtbar, bei einem geschlossenen nicht.<br>
 +
'''boolean leaf''' Den Wert den man erhalten würde, würde man die isLeaf-Methode des Models aufrufen, und value übergeben. Ist der aktuelle Knoten ein Blatt, so ist leaf true, ansonsten ist leaf false.<br>
 +
'''int row''' Betrachtet man den JTree, so sieht man, dass alle Knoten untereinander dargestellt werden. Man kann sie also von obenher nummerieren. row sagt schliesslich, in welcher Zeile der aktuelle Knoten ist.<br>
 +
Mit row kann man sehr interessante Informationen abrufen, z.B. die exakte Position des aktuellen Knotens im Baum (über die Methode JTree#getPathForRow()).<br>
 +
'''boolean hasFocus''' Dieser boolean ist genau dann true, wenn der aktuelle Knoten den Fokus besitzt. Wenn der Benutzer irgendeine Aktion mit der Tastatur macht (z.B. die "Knotenerweiterntaste" drückt), würde sich das auf den Knoten mit dem Fokus auswirken. Normalerweise wird ein Rand um diesen Knoten gezeichnet.
 +
 +
Hier ist der Code eines CellRenderers der zwei Componenten kombiniert. Für die Daten wurde das schon bekannte GraphModel verwendet.
 +
<code=java>
 +
package jtree;
 +
 +
import java.awt.Color;
 +
import java.awt.Component;
 +
import java.awt.GridBagConstraints;
 +
import java.awt.GridBagLayout;
 +
import java.awt.Insets;
 +
 +
import javax.swing.JButton;
 +
import javax.swing.JFrame;
 +
import javax.swing.JLabel;
 +
import javax.swing.JPanel;
 +
import javax.swing.JScrollPane;
 +
import javax.swing.JTree;
 +
import javax.swing.UIManager;
 +
import javax.swing.tree.TreeCellRenderer;
 +
 +
public class Demo6 {
 +
    public static void main( String[] args ) throws Exception {
 +
        // LookAndFeel ändern, ergibt auf einigen Systemen einen
 +
        // beeindruckenderen Effekt
 +
        UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
 +
       
 +
        // Als Model ein GraphModel, da hat man schnell genügend Knoten im Baum
 +
        GraphModel model = new GraphModel( "Abend" );
 +
       
 +
        model.put( "Abend",        "Musikanlage" , "Fernseher" );
 +
       
 +
        model.put( "Musikanlage",  "lauter", "leiser", "abschalten" );
 +
        model.put( "lauter",        "lauter", "leiser", "abschalten" );
 +
        model.put( "leiser",        "lauter", "leiser", "abschalten" );
 +
       
 +
        model.put( "Fernseher",    "zappen", "Werbung", "abschalten" );
 +
        model.put( "zappen",        "zappen", "Werbung", "abschalten" );
 +
        model.put( "Werbung",      "zappen", "Wutanfall" );
 +
        model.put( "Wutanfall",    "Amoklauf" );
 +
        model.put( "Amoklauf" );
 +
       
 +
        model.put( "abschalten",    "Musikanlage", "Fernseher" );
 +
       
 +
        // Den Baum erstellen
 +
        JTree tree = new JTree( model );
 +
       
 +
        // Den CellRenderer setzen, der die Knoten zeichnet
 +
        tree.setCellRenderer( new Renderer() );
 +
       
 +
        // Ein Frame herstellen, um den Tree auch anzuzeigen
 +
        JFrame frame = new JFrame( "JTree - Demo" );
 +
        frame.add( new JScrollPane( tree ));
 +
       
 +
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
 +
        frame.setSize( 400, 400 );
 +
        frame.setLocationRelativeTo( null );
 +
        frame.setVisible( true );
 +
    }
 +
}
 +
 +
// Der neue CellRenderer
 +
class Renderer implements TreeCellRenderer{
 +
    private JPanel panel;
 +
    private JButton icon;
 +
    private JLabel label;
 +
   
 +
    public Renderer(){
 +
        // Man hat völlige Freiheit, wie die Component des Renderers
 +
        // zusammengebaut wird
 +
        panel = new JPanel( new GridBagLayout() );
 +
        icon = new JButton();
 +
        label = new JLabel();
 +
       
 +
        label.setOpaque( true );
 +
       
 +
        panel.add( icon, new GridBagConstraints( 0, 0, 1, 1, 1.0, 1.0,
 +
                GridBagConstraints.WEST, GridBagConstraints.VERTICAL,
 +
                new Insets( 0, 0, 0, 0 ), 0, 0 ));
 +
        panel.add( label, new GridBagConstraints( 1, 0, 1, 1, 1.0, 1.0,
 +
                GridBagConstraints.WEST, GridBagConstraints.BOTH,
 +
                new Insets( 0, 0, 0, 0 ), 0, 0 ));
 +
    }
 +
   
 +
    public Component getTreeCellRendererComponent(
 +
            JTree tree, Object value, boolean selected, boolean expanded,
 +
            boolean leaf, int row, boolean hasFocus ){
 +
       
 +
        // Die Einstellungen kann man nach belieben verändern.
 +
        if( leaf ){
 +
            icon.setBackground( Color.YELLOW );
 +
            icon.setText( "o" );
 +
        }
 +
        else if( expanded ){
 +
            icon.setBackground( Color.RED );
 +
            icon.setText( "+" );
 +
        }
 +
        else{
 +
            icon.setBackground( Color.GREEN );
 +
            icon.setText( "-" );
 +
        }
 +
       
 +
        if( hasFocus )
 +
            label.setBackground( Color.ORANGE );
 +
       
 +
        else if( selected )
 +
            label.setBackground( Color.LIGHT_GRAY );
 +
       
 +
        else
 +
            label.setBackground( tree.getBackground() );
 +
       
 +
       
 +
        label.setText( value.toString() );
 +
       
 +
        return panel;
 +
    }
 +
}
 +
</code=java>
 +
 +
Das TreeModel für den Baum:
 +
<code=java>
 +
class GraphModel implements TreeModel{
 +
    // Die Verbindungen, vom Schlüsselknoten (key) kommt man zu allen Kindknoten (value)
 +
    private Map<String, String[]> connections = new HashMap<String, String[]>();
 +
   
 +
    // Die Wurzel
 +
    private String root;
 +
   
 +
    public GraphModel( String root ){
 +
        this.root = root;
 +
    }
 +
   
 +
    public Object getRoot() {
 +
        return root;
 +
    }
 +
 +
    // Für den Knoten "parent" alle möglichen Kinder angeben.
 +
    public void put( String parent, String...children ){
 +
        connections.put( parent, children );
 +
    }
 +
   
 +
    // Das index'te Kind zurückgeben
 +
    public Object getChild( Object parent, int index ) {
 +
        return connections.get( parent )[index];
 +
    }
 +
 +
    // Die Anzahl Kinder bestimmen
 +
    public int getChildCount( Object parent ) {
 +
        return connections.get( parent ).length;
 +
    }
 +
 +
    // Angabe, ob ein Knoten ein Blatt ist
 +
    public boolean isLeaf( Object node ) {
 +
        return getChildCount( node ) == 0;
 +
    }
 +
 +
    // Den Index eines Knotens bestimmen
 +
    public int getIndexOfChild( Object parent, Object child ) {
 +
        String[] children = connections.get( parent );
 +
        for( int i = 0; i < children.length; i++ )
 +
            if( children[i].equals( child ))
 +
                return i;
 +
       
 +
        return -1;
 +
    }
 +
 +
 +
    public void valueForPathChanged( TreePath path, Object node ) {
 +
        // nicht beachten
 +
    }
 +
   
 +
    public void addTreeModelListener( TreeModelListener listener ) {
 +
        // nicht beachten
 +
    }
 +
 +
    public void removeTreeModelListener( TreeModelListener listener ) {
 +
        // nicht beachten
 +
    }
 +
}
 +
</code=java>
  
  

Version vom 4. September 2013, 13:10 Uhr

Einführung

Der JTree

Der JTree ist eine Component aus dem Package javax.swing. Der JTree wird verwendet um Bäume (wie z.B. die Verzeichnisse auf einer Festplatte) darzustellen, und auch dem Benutzer die Möglichkeit zu geben, diese Bäume zu verändern.

Und sollte jemand diese Componente noch nicht kennen, hier ein Bild: JTree1.png

Kurzer Überblick

Der JTree arbeitet streng mach dem MVC-Pattern:

  • Der JTree selbst ist der Controller
  • Das TreeModel ist das Model
  • Die TreeUI ist schliesslich die (LookAndFeel-abhängige) Implementation der View.

TreeCellRenderer und TreeCellEditor sind Erweiterungen der View, und erlauben die Daten interessanter darzustellen, oder zu verändern.

Verschiedene Listener sorgen für die Kommunikation zwischen den Objekten, so dass z,B. die View stets die aktuellen Daten anzeigt.

Nicht vergessen darf man die Klasse TreePath, welche einen einzelnen Knoten des Baumes eindeutig repräsentiert. Dazu jedoch mehr im letzten Teil dieses Tutorials.

Ressourcen

Im Internet findet man viele Beispiele und Informationen zu dem JTree, hier ein paar Beispiele:

  • Die API ist, wie immer, Ressource Nr. 1. Informationen über den JTree findet man unter javax.swing, genau hier.
  • Eng mit der API verknüft ist "The Java(TM) Tutorial", welches auch ein Kapitel How to Use Trees hat.
  • Weitere Tutorials (wie z.B. das hier) lassen sich mit bekannten Suchmaschinen finden.


Wie bringt man Daten in den JTree?

TreeNode

Die wohl intuitivste Variante Daten in einen JTree zu bringen, sind TreeNodes. Jeder dieser Nodes stellt einen Knoten des Baumes dar, und hat eine beliebige Anzahl Kindknoten. TreeNode selbst ist nur ein Interface. Entweder schreibt man selbst eine Klasse welche das Interface implementiert, oder man benutzt den sehr bequemen DefaultMutableTreeNode.

Die MutableTreeNodes sind nur Wrapper um ein Userobject herum. Das Userobject bestimmt den Wert des Knotens, die Knoten selbst bestimmen die Struktur des Baumes.

Der Baum wird dann schliesslich so aufgebaut: <code=java> package jtree;

import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode;

public class Demo1 {

   public static void main( String[] args ) {
       // Der Wurzelknoten wird hergestellt
       TreeNode root = createTree();
       
       // Der Wurzelknoten wird dem neuen JTree im Konstruktor übergeben
       JTree tree = new JTree( root );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.pack();
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }
   
   private static TreeNode createTree(){
       /*
        * Der Baum wird folgende Form haben:
        * Wurzel
        * +- Buchstaben
        * |  +- A
        * |  +- B
        * |  +- C
        * +- Zahlen
        *    +- 1
        *    +- 2
        *    +- 3
        */
       
       // Zuerst werden alle Knoten hergestellt...
       DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Wurzel" );
       
       DefaultMutableTreeNode letters = new DefaultMutableTreeNode( "Buchstaben" );
       DefaultMutableTreeNode digits = new DefaultMutableTreeNode( "Zahlen" );
       
       DefaultMutableTreeNode letterA = new DefaultMutableTreeNode( "A" );
       DefaultMutableTreeNode letterB = new DefaultMutableTreeNode( "B" );
       DefaultMutableTreeNode letterC = new DefaultMutableTreeNode( "C" );
       
       DefaultMutableTreeNode digit1 = new DefaultMutableTreeNode( "1" );
       DefaultMutableTreeNode digit2 = new DefaultMutableTreeNode( "2" );
       DefaultMutableTreeNode digit3 = new DefaultMutableTreeNode( "3" );
       
       // ... dann werden sie verknüpft
       letters.add( letterA );
       letters.add( letterB );
       letters.add( letterC );
       
       digits.add( digit1 );
       digits.add( digit2 );
       digits.add( digit3 );
       
       root.add( letters );
       root.add( digits );
       
       return root;
   }

} </code=java>

TreeModel

Mit TreeNodes kann man einen Baum sehr einfach aufbauen. Intern arbeitet ein JTree aber stets mit einem TreeModel. Das TreeModel enthält alle Informationen über die Struktur des Baumes. Dafür weiß das TreeModel nicht, wie die Knoten tatsächlich dargestellt werden. Das TreeModel kann für jeden Knoten sagen, wieviele Kinder er besitzt, und wer diese Kinder sind. Die Methode getChildCount() gibt für einen Knoten an, wieviele Kinder er besitzt. Alle Kinder eines Knotens sind durchnummeriert (jeder Knoten ist sozusagen eine Liste von Kindknoten), dementsprechend gibt die Methode getChild() das Kind mit der Nummer "index" zurück. Achtung: getChild() muss für verschiedene Indizes (bei gleichbleibendem Vaterknoten) verschiedene Kinderknoten zurückgeben.

Jeder Baum muss eine Wurzel haben, diese Wurzel wird durch getRoot() angegeben.

Normalerweise besitzt ein Baum Knoten und Blätter. Als Blätter bezeichnet man Knoten die keine Kinder haben. Z.b. in einem Dateisystem entsprechen Verzeichnisse Knoten, und Dateien entsprechen Blätter. Der JTree behandelt Blätter und Knoten leicht unterschiedlich: Knoten kann man aufklappen, Blätter nicht. Ob ein Objekt Knoten oder Blatt ist, bestimmt die Methode isLeaf().

Eine reine Hilfsfunktion ist schließlich getIndexOfChild(), die "Umkehrmethode" von getChild(). Sie gibt an, unter welcher Nummer ein Kindknoten bei einem Vaterknoten gespeichert ist.

Der folgende Code zeigt einen Baum, der als Knoten einfach Strings verwendet. <code=java> package jtree;

import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath;

public class Demo2 {

   public static void main( String[] args ) {
       // Die Verknüpfungen werden hergestellt
       TreeModel model = createTree();
       // Das Model wird dem Konstruktor des JTrees übergeben
       JTree tree = new JTree( model );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.pack();
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }
   
   private static TreeModel createTree(){
       /*
        * Der Baum wird folgende Form haben:
        * Wurzel
        * +- Buchstaben
        * |  +- A
        * |  +- B
        * |  +- C
        * +- Zahlen
        *    +- 1
        *    +- 2
        *    +- 3
        */
       
       TreeModel model = new TreeModel(){
           // Diese Methode gibt die Wurzel des Baumes an.
           public Object getRoot() {
               return "Wurzel";
           }
           // Hier wird das index'te Kind des Knotens "parent" bestummen
           public Object getChild( Object parent, int index ) {
               if( parent.equals( "Wurzel" )){
                   switch( index ){
                       case 0: return "Buchstaben";
                       case 1: return "Zahlen";
                   }
               }
               if( parent.equals( "Buchstaben" )){
                   switch( index ){
                       case 0: return "A";
                       case 1: return "B";
                       case 2: return "C";
                   }
               }
               
               if( parent.equals( "Zahlen" )){
                   switch( index ){
                       case 0: return "1";
                       case 1: return "2";
                       case 2: return "3";
                   }
               }
               
               return null;
           }
           // Gibt für jeden Knoten an, wieviele Kinder er hat
           public int getChildCount( Object parent ) {
               if( parent.equals( "Wurzel" ))
                   return 2;
               
               if( parent.equals( "Zahlen" ))
                   return 3;
               
               if( parent.equals( "Buchstaben" ))
                   return 3;
               
               // Dann wars A, B, C, 1, 2 oder 3
               return 0;
           }
           // Gibt an, ob ein Knoten ein Blatt ist. Ein Blatt kann in einem
           // JTree nicht weiter geöffnet werden.
           // Experiment: immer true oder immer false zurückgeben...
           public boolean isLeaf( Object node ) {
               return getChildCount( node ) == 0;
           }
           // Gibt an, welchen Index der Knoten "child" als Kind vom 
           // Knoten "parent" hat. Wenn man folgenden Code ausführt:
           // 'Object result = getChild( parent, getIndexOfChild( parent, child ))',
           // dann muss "result" und "child" dasselbe Objekt sein. 
           public int getIndexOfChild( Object parent, Object child ){
               int max = getChildCount( parent );
               for( int i = 0; i < max; i++ )
                   if( getChild( parent, i ).equals( child ))
                       return i;
               
               return -1;
           }
           public void addTreeModelListener( TreeModelListener listener ) {
               // siehe später
           }
           public void removeTreeModelListener( TreeModelListener listener ) {
               // siehe später
           }
           public void valueForPathChanged( TreePath path, Object value ) {
               // siehe später
           }
       };
       
       return model;
   }

} </code=java>

Bäume mit unendlich vielen Knoten

Nicht immer ist es möglich, denn gesamten Baum zu kennen. Liest man z.B. einen Baum aus einer Datenbank, könnte man schlicht nicht genügend RAM besitzen, um den gesamten Baum zu speichern. Oder das Erstellen eines Knotens dauert solange (und man hat so viele), dass die Berechnung des Baumes Tage dauern würde.

Ein besonders drastisches Beispiel ist ein Baum mit unendlich vielen Knoten. Selbstverständlich wird es niemals möglich sein, diesen Baum ganzheitlich herzustellen. Trotzdem kann man mit dem JTree auch solche Giganten darstellen. Der Trick dabei ist, dass der JTree das TreeModel erst nach einem Knoten fragt, wenn der Knoten tatsächlich gezeichnet werden soll.
Jetzt erstellt man den Knoten halt erst in dem Augenblick, indem der JTree nach ihm fragt, und da der JTree nur eine begrenzte Zahl von Knoten darstellt... (der Benutzer kann nicht alle Knoten aufeinmal öffnen).
Beachten muss man allerdings, dass ein einmal erstellter Knoten nicht mehr gelöscht werden darf. Denn gibt die getChild()-Methode bei gleichem Input einen anderen Output, wird der JTree ganz verwirrt (und nichts funktioniert mehr).

Im untenstehenden Codeschnipsel besitzt jeder Knoten 10 Unterknoten, die genau dann hergestellt werden, wenn sie das erste Mal abgefragt werden: <code=java> package jtree;

import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath;

public class Demo3 {

   public static void main( String[] args ) {
       // Das Model mit dem unendlichen Baum wird erstellt
       JTree tree = new JTree( new InfiniteModel() );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.setSize( 400, 400 );
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }
   
   

}

// Dieses TreeModel stellt einen Baum mit unendlich vielen Knoten dar class InfiniteModel implements TreeModel{

   private Node root = new Node( "" ){
       @Override
       public String toString() {
           return "Wurzel";
       }
   };
   
   // Der Wurzelknoten
   public Object getRoot() {
       return root;
   }
   // Die Kinder sind in dem parent-Knoten gespeichert
   public Object getChild( Object parent, int index ) {
       Node node = (Node)parent;
       return node.getChild( index );
   }
   // Jeder Knoten in diesem Baum hat exakt 10 Kinder
   public int getChildCount( Object node ) {
       return 10;
   }
   // in diesem Baum haben alle Knoten Kinder
   public boolean isLeaf( Object node ) {
       return false;
   }
   // Den Indes des Kindes bestimmen
   public int getIndexOfChild( Object parent, Object child ) {
       Node node = (Node)parent;
       for( int i = 0; i < 10; i++ )
           if( node.getChild( i ) == child )
               return i;
       
       return -1;
   }
   public void valueForPathChanged( TreePath path, Object node ) {
       // nicht beachten
   }
   
   public void addTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }
   public void removeTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }
   
   // Der unendliche Baum wird durch diese Node-Objekte dargestellt. Bei
   // Bedarf kann sich so ein Nodeobjekt selbstständig neue Kinder erschaffen.
   private class Node{
       private String name;
       private Node[] children;
       
       public Node( String name ){
           this.name = name;
       }
       
       // Gibt das index'te Kind dieses Nodes zurück
       public Node getChild( int index ){
           ensureChildren();
           return children[ index ];
       }
       
       // Sorgt dafür, dass dieser Knoten wirklich Kinder hat
       private void ensureChildren(){
           if( children == null ){
               children = new Node[ 10 ];
               for( int i = 0; i < 10; i++ )
                   children[i] = new Node( name + i );
           }
       }
       
       // Der JTree ruft die "toString"-Methode auf, um den Namen eines 
       // Knoten-Objektes zu erfahren. ßberschreibt man die Methode, kann
       // man andere Namen anzeigen lassen
       @Override
       public String toString() {
           return name;
       }
   }

} </code=java>

Knoten mehrfach verwenden

Betrachtet man das TreeModel genauer, fällt auf, dass man nur mit dem Model alleine nicht herausfinden kann, wer denn der Vater von irgendeinem Knoten ist. Mit anderen Worten: das TreeModel erlaubt es, Knoten an verschiedenen Stellen mehrfach zu verwenden. Eine konkrete Anwendung könnte eine Liste sein, welche auf verschiedene Weisen sortiert ausgegeben werden kann. In diesem Fall gibt es Knoten "Sortiert nach X", "Sortiert nach Y", ... und diese Knoten besitzen jeweils dieselben Kinder, jediglich in unterschiedlicher Reihenfolge.

Eine solche Datenstruktur nennt man Graph. Will man jeden Knoten nur einmal zeichnen, ist ein Graph ist (im Allgemeinen) nicht als Baum darstellbar. Darf man hingegen die Knoten mehr als einmal zeichnen, hat man keine Probleme. Der JTree ist in der Lage, denselben Knoten an verschiedenen Stellen zu zeichnen, kann daher auch Graphen darstellen (auch wenn diese Darstellung nicht unbedingt übersichtlich ist...)

Im Gegensatz zu den anderen Beispielen dieses Kapitels, ist es nicht möglich einen Graph mit TreeNodes darzustellen.

Das folgende Codestück zeigt einen solchen Graphen. Für jeden Knoten gibt es eine ganze Reihe von Folgeknoten, man kann stundenlang Knoten im JTree öffnen, und trotzdem wird man nie mehr als 13 unterschiedliche Knoten zu sehen bekommen. <code=java> package jtree;

import java.util.HashMap; import java.util.Map;

import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath;

public class Demo4 {

   public static void main( String[] args ) {
       // Das Model füllen
       GraphModel model = new GraphModel( "Tagesanbruch" );
       
       model.put( "Tagesanbruch",      "kochen", "schlafen" );
       model.put( "kochen",            "versalzen", "angebrannt" , "gekocht" );
       model.put( "versalzen",         "kochen" );
       model.put( "angebrannt",        "abwaschen", "trotzdem essen" );
       model.put( "trotzdem essen",    "vergiftet" );
       model.put( "vergiftet" );
       model.put( "gekocht",           "essen" );
       model.put( "essen",             "abwaschen", "stehen lassen" );
       model.put( "stehen lassen",     "stehen lassen", "abwaschen" );
       model.put( "abwaschen",         "kochen", "schlafen" );
       model.put( "schlafen",          "schlafen", "schnarchen", "aufwachen" );
       model.put( "schnarchen",        "aufwachen" );
       model.put( "aufwachen",         "schlafen", "kochen" );
       
       // Der Wurzelknoten wird dem neuen JTree im Konstruktor übergeben
       JTree tree = new JTree( model );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.setSize( 400, 400 );
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }    

}

class GraphModel implements TreeModel{

   // Die Verbindungen, vom Schlüsselknoten (key) kommt man zu allen Kindknoten (value)
   private Map<String, String[]> connections = new HashMap<String, String[]>();
   
   // Die Wurzel
   private String root;
   
   public GraphModel( String root ){
       this.root = root;
   }
   
   public Object getRoot() {
       return root;
   }
   // Für den Knoten "parent" alle möglichen Kinder angeben.
   public void put( String parent, String...children ){
       connections.put( parent, children );
   }
   
   // Das index'te Kind zurückgeben
   public Object getChild( Object parent, int index ) {
       return connections.get( parent )[index];
   }
   // Die Anzahl Kinder bestimmen
   public int getChildCount( Object parent ) {
       return connections.get( parent ).length;
   }
   // Angabe, ob ein Knoten ein Blatt ist
   public boolean isLeaf( Object node ) {
       return getChildCount( node ) == 0;
   }
   // Den Index eines Knotens bestimmen
   public int getIndexOfChild( Object parent, Object child ) {
       String[] children = connections.get( parent );
       for( int i = 0; i < children.length; i++ )
           if( children[i].equals( child ))
               return i;
       
       return -1;
   }


   public void valueForPathChanged( TreePath path, Object node ) {
       // nicht beachten
   }
   
   public void addTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }
   public void removeTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }

} </code=java>


Darstellung der Daten

Der JTree stellt jeden Knoten einzeln dar. Man hat dabei verschiedene Möglichkeiten die Darstellung drastisch zu verändern:

Standardeinstellung

Normalerweise ruft der JTree einfach für jedes Knoten-Objekt die toString()-Methode auf. Man kann also diese Methode überschreiben, einen anderen String zurückgeben, und schon steht ein anderer Text beim Knoten.

Der DefaulTreeCellRenderer

Schon ein bisschen raffinierter als die toString()-Methode zu überschreiben, ist es, den TreeCellRenderer des JTrees auszutauschen. Man kann dies mit Hilfe der Methode setCellRenderer() erreichen.

Am einfachsten ist es wohl, von DefaultTreeCellRenderer zu erben, und die Methode getTreeCellRendererComponent zu überschreiben. Diese Methode wird vom JTree immer dann aufgerufen, wenn ein Knoten dargestellt werden soll (eine genauere Erklärung folgt in Abschnitt 3. TreeCellRenderer).
In der überschriebenen getTreeCellRendererComponent-Methode setzt man verschiedene Einstellungen, z.B. Hintergrundfarbe, Icon, Text, ... des DefaultTreeCellRenderers. Zum Schluss beendet man die Methode mit dem Befehl return this; <code=java> package jtree;

import java.awt.Color; import java.awt.Component;

import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreeNode;

public class Demo5 {

   public static void main( String[] args ) {
       // Den Baum mit den Farben erstellen
       JTree tree = new JTree( createTree() );
       
       // Den CellRenderer setzen, der die Knoten zeichnet
       tree.setCellRenderer( new ColorRenderer() );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.setSize( 400, 400 );
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }
   
   private static TreeNode createTree(){
       // TreeNodes mit String und Color-Objekten herstellen
       DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Farben" );
       
       DefaultMutableTreeNode red = new DefaultMutableTreeNode( "Rot" );
       DefaultMutableTreeNode green = new DefaultMutableTreeNode( "Grün" );
       DefaultMutableTreeNode blue = new DefaultMutableTreeNode( "Blau" );
       
       root.add( red );
       root.add( green );
       root.add( blue );
       
       for( float f = 0; f <= 1f; f += 0.05f )
           red.add( new DefaultMutableTreeNode( new Color( f, 0, 0 )));
       
       for( float f = 0; f <= 1f; f += 0.05f )
           green.add( new DefaultMutableTreeNode( new Color( 0, f, 0 )));
       
       for( float f = 0; f <= 1f; f += 0.05f )
           blue.add( new DefaultMutableTreeNode( new Color( 0, 0, f )));
       
       return root;
   }   

}

class ColorRenderer extends DefaultTreeCellRenderer{

   public ColorRenderer(){
       // Versichern, dass der Hintergrund gezeichnet wird
       setOpaque( true );
   }
   
   @Override
   public Component getTreeCellRendererComponent( 
           JTree tree, Object value, boolean sel, boolean expanded, 
           boolean leaf, int row, boolean hasFocus ){
       // Die Originalmethode die Standardeinstellungen machen lassen
       super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus );
       
       // Den Wert des Knotens abfragen
       DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
       Object inside = node.getUserObject();
       
       // Wenn der Knoten eine Farbe ist: den Hintergrund entsprechend setzen
       // Andernfalls soll der Hintergrund zum Baum passen.
       if( inside instanceof Color ){
           setBackground( (Color)inside );
           setForeground( Color.WHITE );
       }
       else{
           setBackground( tree.getBackground() );
           setForeground( tree.getForeground() );
       }
       
       return this;
   }

} </code=java>

Der TreeCellRenderer

Der DefaultTreeCellRenderer ist zwar nett zu benutzen, aber die volle Kapazität hat man damit noch nicht erreicht. Weitaus mehr Gestaltungsmöglichkeit besitzt man, wenn man das Interface TreeCellRenderer direkt implementiert.
Um einen TreeCellRenderer richtig zu implementieren, muss man etwas über den internen Ablauf beim Zeichnen des JTrees wissen. Pseudocodeartig sieht der so aus: <code=java> public class PseudoJTree{

 public void paint( Graphics g ){
   // Der aktuelle TreeCellRenderer
   TreeCellRenderer renderer = ... 
   // Alle sichtbaren Knoten werden aufgerufen
   for( Object node : visibleNodes ){
     // Der Renderer wird benutzt um eine java.awt.Component bereitzustellen,
     // welche dann den Knoten zeichnen wird.
     Component component = renderer.getTreeCellRendererComponent( this, node, ... );
     // Die Component wird an die richtige Position gesetzt, und ihre Grösse kann
     // verändert werden.
     component.setBounds( ... );
     // Die Component zeichnet nun den Knoten
     component.paint( g );
   }
 }

} </code=java>

Da ergeben sich nun mehrere Beobachtungen, die man beim Implementieren beachten sollte:

  • Der JTree verwendet ein einziges TreeCellRenderer-Objekt. Das bedeutet, dass es absolut unmöglich ist, im TreeCellRenderer irgendwelche Daten zu speichern, wie ein bestimmter Knoten aussehen soll. Alle Daten gehören in das TreeModel!
  • Die Methode des Renderers kann sehr oft aufgerufen werden. Um den Garbage Collector nicht zusehr zu belasten, sollte man zurückhaltend bei der Erstellung neuer Objekte sein.
  • Die Methode des Renderers wird (indirekt) von paint aus aufgerufen. Die paint-Methode ist ein kritischer Abschnitt, wenn es um die gefühlte Geschwindigkeit des Programmes geht. Da die Methode auch noch mehrfach aufgerufen wird, muss gewährleistet sein, dass sie schnellstmöglich an das Ende kommt. Man muss nicht gerade jede Nanosekunde aus dem Code pressen, aber "mal kurz" ein Icon von der Festplatte einlesen, ist definitiv eine äusserst schlechte Idee. Hier sollte man soviel wie möglich im Voraus berechnen und speichern.
  • Die Component die der CellRenderer erstellt, wird einmal verwendet, und dann fortgeworfen. Idealerweise verwendet man immer wieder dasselbe Component-Objekt, und ändert in der getTreeCellRendererComponent nur ein paar wenige Einstellungen dieser Component.
  • Die Component wird dem JTree nicht hinzugefügt (kein Aufruf von add). Daher ist keine Benutzerinteraktion mit dem Renderer möglich. Mouse- und KeyListener werden z.B. mit Garantie niemals aufgerufen.


Die getTreeCellRendererComponent-Methode hat eine ganze Reihe von Argumenten. Für jedes eine kurze Beschreibung.

JTree tree Das ist die JTree-Instanz, für die der Renderer etwas zeichnen soll (der Renderer kann von mehreren JTrees gleichzeitig benutzt werden).
Object value Das ist der Knoten, der gezeichnet werden soll. Dieses Objekt stammen vom TreeModel und wurden über die Methoden getChild() oder getRoot() abgerufen.
boolean selected Gibt an, ob der Knoten vom Benutzer selektiert wurde. Üblicherweise wird dann ein anderer Hintergrund verwendet.
boolean expanded Gibt an, ob der aktuelle Knoten geöffnet wurde. Bei einem geöffneten Knoten sind all seine Kinder sichtbar, bei einem geschlossenen nicht.
boolean leaf Den Wert den man erhalten würde, würde man die isLeaf-Methode des Models aufrufen, und value übergeben. Ist der aktuelle Knoten ein Blatt, so ist leaf true, ansonsten ist leaf false.
int row Betrachtet man den JTree, so sieht man, dass alle Knoten untereinander dargestellt werden. Man kann sie also von obenher nummerieren. row sagt schliesslich, in welcher Zeile der aktuelle Knoten ist.
Mit row kann man sehr interessante Informationen abrufen, z.B. die exakte Position des aktuellen Knotens im Baum (über die Methode JTree#getPathForRow()).
boolean hasFocus Dieser boolean ist genau dann true, wenn der aktuelle Knoten den Fokus besitzt. Wenn der Benutzer irgendeine Aktion mit der Tastatur macht (z.B. die "Knotenerweiterntaste" drückt), würde sich das auf den Knoten mit dem Fokus auswirken. Normalerweise wird ein Rand um diesen Knoten gezeichnet.

Hier ist der Code eines CellRenderers der zwei Componenten kombiniert. Für die Daten wurde das schon bekannte GraphModel verwendet. <code=java> package jtree;

import java.awt.Color; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets;

import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.UIManager; import javax.swing.tree.TreeCellRenderer;

public class Demo6 {

   public static void main( String[] args ) throws Exception {
       // LookAndFeel ändern, ergibt auf einigen Systemen einen 
       // beeindruckenderen Effekt
       UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
       
       // Als Model ein GraphModel, da hat man schnell genügend Knoten im Baum
       GraphModel model = new GraphModel( "Abend" );
       
       model.put( "Abend",         "Musikanlage" , "Fernseher" );
       
       model.put( "Musikanlage",   "lauter", "leiser", "abschalten" );
       model.put( "lauter",        "lauter", "leiser", "abschalten" );
       model.put( "leiser",        "lauter", "leiser", "abschalten" );
       
       model.put( "Fernseher",     "zappen", "Werbung", "abschalten" );
       model.put( "zappen",        "zappen", "Werbung", "abschalten" );
       model.put( "Werbung",       "zappen", "Wutanfall" );
       model.put( "Wutanfall",     "Amoklauf" );
       model.put( "Amoklauf" );
       
       model.put( "abschalten",    "Musikanlage", "Fernseher" );
       
       // Den Baum erstellen
       JTree tree = new JTree( model );
       
       // Den CellRenderer setzen, der die Knoten zeichnet
       tree.setCellRenderer( new Renderer() );
       
       // Ein Frame herstellen, um den Tree auch anzuzeigen
       JFrame frame = new JFrame( "JTree - Demo" );
       frame.add( new JScrollPane( tree ));
       
       frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
       frame.setSize( 400, 400 );
       frame.setLocationRelativeTo( null );
       frame.setVisible( true );
   }

}

// Der neue CellRenderer class Renderer implements TreeCellRenderer{

   private JPanel panel;
   private JButton icon;
   private JLabel label;
   
   public Renderer(){
       // Man hat völlige Freiheit, wie die Component des Renderers
       // zusammengebaut wird
       panel = new JPanel( new GridBagLayout() );
       icon = new JButton();
       label = new JLabel();
       
       label.setOpaque( true );
       
       panel.add( icon, new GridBagConstraints( 0, 0, 1, 1, 1.0, 1.0, 
               GridBagConstraints.WEST, GridBagConstraints.VERTICAL,
               new Insets( 0, 0, 0, 0 ), 0, 0 ));
       panel.add( label, new GridBagConstraints( 1, 0, 1, 1, 1.0, 1.0, 
               GridBagConstraints.WEST, GridBagConstraints.BOTH,
               new Insets( 0, 0, 0, 0 ), 0, 0 ));
   }
   
   public Component getTreeCellRendererComponent( 
           JTree tree, Object value, boolean selected, boolean expanded, 
           boolean leaf, int row, boolean hasFocus ){
       
       // Die Einstellungen kann man nach belieben verändern.
       if( leaf ){
           icon.setBackground( Color.YELLOW );
           icon.setText( "o" );
       }
       else if( expanded ){
           icon.setBackground( Color.RED );
           icon.setText( "+" );
       }
       else{
           icon.setBackground( Color.GREEN );
           icon.setText( "-" );
       }
       
       if( hasFocus )
           label.setBackground( Color.ORANGE );
       
       else if( selected )
           label.setBackground( Color.LIGHT_GRAY );
       
       else
           label.setBackground( tree.getBackground() );
       
       
       label.setText( value.toString() );
       
       return panel;
   }

} </code=java>

Das TreeModel für den Baum: <code=java> class GraphModel implements TreeModel{

   // Die Verbindungen, vom Schlüsselknoten (key) kommt man zu allen Kindknoten (value)
   private Map<String, String[]> connections = new HashMap<String, String[]>();
   
   // Die Wurzel
   private String root;
   
   public GraphModel( String root ){
       this.root = root;
   }
   
   public Object getRoot() {
       return root;
   }
   // Für den Knoten "parent" alle möglichen Kinder angeben.
   public void put( String parent, String...children ){
       connections.put( parent, children );
   }
   
   // Das index'te Kind zurückgeben
   public Object getChild( Object parent, int index ) {
       return connections.get( parent )[index];
   }
   // Die Anzahl Kinder bestimmen
   public int getChildCount( Object parent ) {
       return connections.get( parent ).length;
   }
   // Angabe, ob ein Knoten ein Blatt ist
   public boolean isLeaf( Object node ) {
       return getChildCount( node ) == 0;
   }
   // Den Index eines Knotens bestimmen
   public int getIndexOfChild( Object parent, Object child ) {
       String[] children = connections.get( parent );
       for( int i = 0; i < children.length; i++ )
           if( children[i].equals( child ))
               return i;
       
       return -1;
   }


   public void valueForPathChanged( TreePath path, Object node ) {
       // nicht beachten
   }
   
   public void addTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }
   public void removeTreeModelListener( TreeModelListener listener ) {
       // nicht beachten
   }

} </code=java>

-- Beni (03.12.2005, 14:25)