JTree - Teil 5 - Benutzerinteraktion

Status
Nicht offen für weitere Antworten.
B

Beni

Gast
JTree - Teil 5 - Benutzerinteraktion
Um dem Benutzer die Möglichkeit der direkten Interaktion zu geben, kann man den JTree leicht erweitern.
Als erstes muss man die Methode setEditable des JTrees aufrufen, und true übergeben (per Default ist false gesetzt).

1. Standardmechanismus
Benutzt man das DefaultTreeModel, und setzt den JTree editable, so kann man einen Doppelklick auf einen Knoten machen, und einen neuen String eingeben. Das DefaultTreeModel geht davon aus, dass man einen MutableTreeNode verändern will. Es wird den neu eingegebenen String direkt in den MutableTreeNode schreiben. Sollte man keine MutableTreeNodes verwenden, gibts lediglich eine ClassCastException.

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

public class Demo8 {
    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 );
       
        // Dem Tree das verändern der Knoten erlauben
        tree.setEditable( true );
       
        // 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(){
        /*
         * 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;
    }
}

2. TreeCellEditor
Nicht immer möchte man Strings in dem Baum speichern. Oder manchmal möchte man einige zusätzliche Bedingungen für Strings definieren (z.B. dass die Maximallänge 5 Zeichen ist).
Für solche Fälle erweitert man entweder den DefaultTreeCellEditor, oder implementiert selbst das Interface TreeCellEditor.


Ein TreeCellEditor ist immer auch ein CellEditor, dementsprechend müssen auch die Methoden des CellEditors implementiert werden.
Es folgen nun die Methoden die man implementieren muss:
  • Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row)
    Die wichtigste Methode des CellEditors. Die Methode wird direkt vom JTree tree aufgerufen, wenn der Benutzer ein Element des Baumes editieren will. Das editierte Element wird als value übergeben, der CellEditor generiert daraufhin eine Component, welche diesen Wert irgendwie darstellt. Die anderen 4 Parameter der Methode, isSelected, expanded, leaf, und row haben eine mindere Bedeutung, sie könnten z.B. benutzt werden, um die Darstellung leicht zu verändern...
    Die Component, welche getTreeCellEditorComponent zurückgibt, sollte dem CellEditor bekannt bleiben, denn der JTree wird denn CellEdtiro später fragen, was denn nun auf dieser Component dargestellt wird.
    Da der Benutzer immernur an einer Stelle editieren kann, kann der Editor auch stets dieselbe Component zurückgeben (ein Editor der direkt von JTextField erbt, und sich selbst zurückgibt, ist also möglich).
  • void addCellEditorListener(CellEditorListener l)
    Der Editor kann selbst entscheiden, wann die Eingabe fertig ist (z.B. könnte ENTER bedeuten, dass der Benutzer fertig editiert hat, während ESC für "Abbrechen" steht...).
    Diese Entscheidungen müssen mindestens dem JTree, vielleicht auch weiteren Objekten, mitgeteilt werden. Wer immer sich für diese Entscheidungen interessiert, kann ein CellEditorListener registrieren. Es ist die Pflicht des Editors bei jeder Entscheidung alle registrieren Listeners aufzurufen.
  • void removeCellEditorListener(CellEditorListener l)
    Entfernt früher hinzugefügte Listener.
  • void cancelCellEditing()
    Wird diese Methode aufgerufen, soll der Editor sofort mit dem Editieren aufhören, und die Veränderungen nicht akzeptieren. Bei einem Aufruf dieser Methode müssen die CellEditorListener informiert werden.
  • boolean stopCellEditing()
    Wird diese Methode aufgerufen, soll der Editor sofort mit dem Editieren aufhören. Der veränderte Wert wird in den Baum zurückgeschrieben.
    Manchmal ist aber das, was der Benutzer eingegeben hat, schlicht falsch. Bei einem JTextField-Editor mag man zwar einen leeren String eingeben können, die Logik des Baumes benötigt aber trotzdem mindestens ein Zeichen. Sollte also die Eingabe nicht zur Programmlogik passen, kann der Editor hier einfach false zurückgeben, und wird dann weiterhin angezeigt.
    Sollte der Editor aber tatsächlich stoppen, müssen alle registrierten CellEditorListeners unterrichtet werden.
  • Object getCellEditorValue()
    Gibt das veränderte Objekt zurück, welches der Editor derzeit darstellt. Ist die Editor-Component ein JTextField könnte der Rückgabewert einfach der String des Textfeldes sein.
  • boolean isCellEditable(EventObject anEvent)
    Diese Methode wird aufgerufen um zu entscheiden, ob die Aktionen eines Benutzers ausreichen, um den Editor erscheinen zu lassen. Ein Editor könnte zum Beispiel darauf bestehen, dass der Benutzer einen Doppelklick mit der Maus machen muss, oder dass der Benutzer irgendeine Taste (mit Ausnahme der Taste "g") drücken muss.
  • boolean shouldSelectCell(EventObject anEvent)
    Wenn diese Methode true zurückgibt (was sie normalerweise tut), wird die Editor-Component selektiert. Wenn sie false zurückgibt, geschieht halt einfach nichts.

Der folgende Code zeigt einen Editor, der eine JComboBox ausklappt. Für die Struktur des Baumes werden DefaultMutableTreeNodes verwendet, welche zusammen mit dem DefaultTreeModel die ganze Organisation von wegen "Knoten austauschen" übernehmen. Jeder Knoten wird durch ein Entry-Objekt (Code, siehe unten) dargestellt.
Code:
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventObject;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeNode;

public class Demo9 {
    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 );
       
        // Der Tree soll editierbar mit dem vorgegebenen Editor sein
        tree.setEditable( true );
        tree.setCellEditor( new ComboBoxEditor());
       
        // 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(){
        /*
         * Der Baum wird folgende Form haben:
         * Wurzel/Wurzelzwerg/Wurzelknolle
         * +- Buchstaben/Letter
         * |  +- A/B/C
         * |  +- A/B/C
         * |  +- A/B/C
         * +- Zahlen/Digits
         *    +- 1/2/3
         *    +- 1/2/3
         *    +- 1/2/3
         */
       
        // Zuerst werden alle Knoten hergestellt...
        DefaultMutableTreeNode root = new DefaultMutableTreeNode( new Entry( 0, "Wurzel", "Wurzelzwerg", "Wurzelknolle" ) );
       
        DefaultMutableTreeNode letters = new DefaultMutableTreeNode( new Entry( 0, "Buchstaben", "Letters" ));
        DefaultMutableTreeNode digits = new DefaultMutableTreeNode( new Entry( 0, "Zahlen", "Digits" ));
       
        DefaultMutableTreeNode letterA = new DefaultMutableTreeNode( new Entry( 0, "A", "B", "C" ) );
        DefaultMutableTreeNode letterB = new DefaultMutableTreeNode( new Entry( 1, "A", "B", "C" ) );
        DefaultMutableTreeNode letterC = new DefaultMutableTreeNode( new Entry( 2, "A", "B", "C" ) );
       
        DefaultMutableTreeNode digit1 = new DefaultMutableTreeNode( new Entry( 0, "1", "2", "3" ) );
        DefaultMutableTreeNode digit2 = new DefaultMutableTreeNode( new Entry( 1, "1", "2", "3" ) );
        DefaultMutableTreeNode digit3 = new DefaultMutableTreeNode( new Entry( 2, "1", "2", "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;
    }
}

// Entry ist ein Eintrag in den Baum. Jeder Eintrag besteht aus einer Liste
// von möglichten Werten, die der Eintrag annehmen kann, sowie einem Indes
// der aktuell ausgewählten Möglichkeit
class Entry{
    // Die Liste der Wahlmöglichkeiten
    private String[] choices;
   
    // Die aktuelle Wahl
    private int choice = 0;
   
    public Entry( int choice, String...choices ){
        this.choice = choice;
        this.choices = choices;
    }
   
    public int getChoiceCount(){
        return choices.length;
    }
   
    public String getChoice( int index ){
        return choices[index];
    }
   
    public int getChoice(){
        return choice;
    }
   
    public void setChoice( int choice ){
        this.choice = choice;
    }
   
    @Override
    public String toString() {
        // Da der DefaultTreeCellRenderer verwendet wird, und dieser Renderer
        // die "toString"-Methode benutzt, soll diese Methode die aktuelle
        // Wahl zurückgeben.
        return getChoice( getChoice() );
    }
}

// Der Editor des JTrees. Dieser Editor wird durch einen Doppelklick mit
// der Maus aktiviert. Er zeigt eine ComboBox, auf der man eine Auswahl
// treffen kann, sobald man die Auswahl gemacht hat, "schliesst" sich der
// Editor wieder.
class ComboBoxEditor implements TreeCellEditor{
    // die Liste aller registrierten Listener
    private Collection<CellEditorListener> listeners = new ArrayList<CellEditorListener>();
   
    // Das Model der ComboBox (die Liste, was die ComboBox enthält...)
    private DefaultComboBoxModel model;
   
    // Die Box, die immer wieder angezeigt wird
    private JComboBox box;
   
    public ComboBoxEditor(){
        model = new DefaultComboBoxModel();
        box = new JComboBox( model );
       
        // Sobald eine Auswahl gemacht wird, soll der Editor "stoppen" (und
        // der Wert wird in den Baum geschrieben).
        box.addActionListener( new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                stopCellEditing();
            }
        });
    }
   
    public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
        // Sollte der Editor gerade sonstwo benutzt werden, könnte eine
        // Mischung der "getTreeCellEditorComponent"-Methode sehr "explosiv" sein...
        cancelCellEditing();
       
        // Dem ComboBox-Model sagen, welche Auswahlen möglich sind, und was
        // aktuell ausgewählt ist. All diese Informationen stehen in dem
        // Entry-Objekt, welches vom JTree übergeben wird.
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
        Entry entry = (Entry)node.getUserObject();
        model.removeAllElements();
       
        for( int i = 0, n = entry.getChoiceCount(); i<n; i++ )
            model.addElement( entry.getChoice( i ) );
       
        model.setSelectedItem( model.getElementAt( entry.getChoice() ));
       
        // Die neu konfigurierte ComboBox zurückgeben
        return box;
    }

    // Gibt den aktuellen Wert des Editors zurück. Dies kann dasselbe Objekt
    // sein, welches bei "getTreeCellEditorComponent" übergeben wurde, kann
    // aber auch ein total neues Objekt sein. In der vorliegenden Implementation
    // wird ein neues Objekt hergestellt
    public Object getCellEditorValue() {
        // Mit den Daten des ComboBox-Models ein "Entry" erzeugen.
        String[] choices = new String[ model.getSize() ];
        for( int i = 0; i < choices.length; i++ )
            choices[i] = (String)model.getElementAt( i );
       
        int selected = model.getIndexOf( model.getSelectedItem() );
        return new Entry( selected, choices );
    }

    public boolean isCellEditable(EventObject anEvent) {
        // Bei einem Doppelklick soll der Benutzer editieren können. Die anderen
        // Events interessieren nicht, aber zur Sicherheit wird einfach mal
        // "true" zurückgegeben. Vielleicht will ja jemand mit der Tastatur
        // was machen...
        if( anEvent instanceof MouseEvent ){
            return ((MouseEvent)anEvent).getClickCount() >= 2;
        }
        else
            return true;
    }

    public boolean shouldSelectCell(EventObject anEvent) {
        return true;
    }

    public boolean stopCellEditing() {
        // Unterrichtet alle Listener, dass nicht mehr editiert wird,
        // und dass das die neue Eingabe akzeptiert wurde.
        ChangeEvent event = new ChangeEvent( this );
       
        for( CellEditorListener listener : listeners )
            listener.editingStopped( event );
       
        return true;
    }

    public void cancelCellEditing() {
        // Informiert alle Listener, dass nicht mehr editiert wird,
        // und dass die Eingabe verworfen wurde.
        ChangeEvent event = new ChangeEvent( this );
       
        for( CellEditorListener listener : listeners )
            listener.editingCanceled( event );
    }

    public void addCellEditorListener(CellEditorListener listener) {
        listeners.add( listener );
    }

    public void removeCellEditorListener(CellEditorListener listener) {
        listeners.remove( listener );
    }
}

3. Verbindung zum TreeModel
Wenn man ein eigenes TreeModel verwendet, muss man sich auch selbst um die veränderten Knoten kümmern. Wann immer der Benutzer einen TreeCellEditor benutzt hat, um ein Knoten abzuändern, wird die Methode valueForPathChanged aufgerufen. Der erste Parameter ist der Pfad des veränderten Knotens, der zweite Parameter ist der Rückgabewert von CellEditor#getCellEditorValue.
Wie das Model auf die Veränderung reagiert, ist ihm freigestellt. Das kann von ignorieren bis zu gleichzeitigem Entfernen/Einfügen von Kindern gehen... in jedem Fall müssen die TreeModelListener entsprechend aufgerufen werden.
 
Zuletzt bearbeitet von einem Moderator:
Status
Nicht offen für weitere Antworten.

Neue Themen


Oben