JTree - Teil 3 - Darstellung der Daten

Status
Nicht offen für weitere Antworten.
B

Beni

Gast
JTree - Teil 3 - Darstellung der Daten
Der JTree stellt jeden Knoten einzeln dar. Man hat dabei verschiedene Möglichkeiten die Darstellung drastisch zu verändern:

1. 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.

2. Der DefaulTreeCellRenderer
Schon ein bisschen rafinierter als die toString-Methode zu überschreiben, ist es, den TreeCellRenderer des JTrees auszutauschen. Man kann macht dies mit Hilfe der Methode setCellRenderer.

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:
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;
    }
}

3. 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 ein bisschen was über den internen Ablauf beim Zeichnen des JTrees wissen. Pseudocordartig sieht der so aus:
Code:
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 );
    }
  }
}

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 vorraus 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 selectedGibt 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.

tree_03.png


Code:
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;
    }
}

Das TreeModel für den Baum
Code:
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
    }
}
 
Zuletzt bearbeitet von einem Moderator:
Status
Nicht offen für weitere Antworten.

Neue Themen


Oben