Java Swing JTree mit dem SwingWorker

31.07.2011

Einleitung

Im Folgenden wird am Beispiel eines JTree erklärt, wie man den Java SwingWorker einsetzt, um zu verhindern, daß sich eine Swing-Oberfläche nicht neu zeichnet (repaint()), während ein Task oder Prozess lange läuft.

In dem Beispiel wird ein JTree erstellt, der auf jeder Ebene 10 Knoten hat. Wird ein Knoten aufgeklappt (expand), dann wird ein Waiting-Node in den Baum eingefügt. Dieser wird so lange angezeigt bis die 10 Kindknoten erzeugt sind. Dann wird der Knoten wieder entfernt und die neuen 10 Kindknoten werden in den Baum eingefügt. In dem Beispiel wird eine künstliche Wartezeit von 10 Sekunden eingebaut, damit der Waiting-Node überhaupt zu sehen ist.

Wer schon einmal mit Swing versucht hat eine Oberfläche zu programmieren, wird den Effekt kennen, daß die Anzeige bei langlaufenden Prozessen nicht neu gezeichnet wird, solange der Hintergrundprozess am Arbeiten ist. Wird beispielsweise ein JTree aufgeklappt und die Erstellung der Knoten dauert mehrere Minuten (z.B. beim Remote-Zugriff auf Daten), dann bleibt die Oberfläche schwarz sobald das Fenster einmal im Bildschirmhintergrund war. Ein anderes Beispiel ist ein Fortschrittsbalken. Man startet einen Prozeß und erwartet, daß sich der Balken Schritt für Schritt bewegt, aber es ist so, daß lange Zeit gar nichts passiert und dann Balken bei 100% steht.

Dieses Phänomen ist seit Anbeginn der Zeit tief in Java verwurzelt. Bei Java 1.0 bis 1.2 hat das auch noch niemanden interessiert. In dieser Zeit war man froh, daß es überhaupt möglich war, eine grafische Oberfläche zu erstellen. Bei Java 1.3 und 1.4 wurde man sich dieses Problems bewußt und ein Sun-Mitarbeiten hat dankenswerterweise einen Artikel mit einer Lösung veröffentlicht. Dieser Artikel ist hier zu finden. Using a Swing Worker Thread.

Bei Java 5 wurde der SwingWorker dann endlich Teil des JDK und man mußte nicht mehr seine eigene SwingWorker-Klasse in jedes Projekt hinzufügen. Die Benutzung des SwingWorkers ist zwar immer noch umständlich, aber es wird zumindest eine Standard-Lösung angeboten. Ein weiterer Vorteil ist, daß eine einheitliche Schnittstelle verwendet werden kann. Bevor der SwingWorker Teil des JDK geworden ist, gab es zahlreiche unterschiedliche Implementierungen, die sich häufig nur durch die Benennung der Methoden unterschieden haben. Dies hat oft zu Verwirrungen geführt. Die Folge war, daß in größeren Projekten nach kurzer Zeit mehrere SwingWorker-Implementierungen Teil des Programmcodes waren. Und das widerspricht bekanntlicherweise dem DRY-Prinzip (Don't repeat yourself). :-)

Die folgende Abbild zeigt das fertige Beispiel, das in dem Artikel nach und nach entsteht.

Der SwingWorker

Der SwingWorker ist im Package javax.swing.SwingWorker zu finden.

Der Grundgedanke des SwingWorkers ist es, den langlaufenden Prozeß und das Zeichnen aufzuteilen. Also folgendermaßen:

  • Einmal der langlaufende Prozeß. In dem Beispiel das Erzeugen der Knoten.
  • Und als zweites das Neuzeichnen der Oberfläche. In dem Beispiel das Zeichnen der neuen Knoten.

Für den ersten Teil gibt es die Methode public Object doInBackground(). In dem folgenden Beispiel wird eine Liste mit Knoten von Typ MyNode erstellt.

public Object doInBackground() {
    List<MyNode> children = new ArrayList<MyNode>();

    for ( int i = 0; i < 10; i++ ) {
    	MyNode child = new MyNode( "Knoten "+ Integer.toString( i ) );
        children.add( child );
    }

    return children;
}

Für das Zeichnen existiert die Methode public void done(). Als erstes wird der Waiting-Node entfernt. Danach wird das Ergebnis von doInBackground() mit der Methode get() abgefragt und die erstellten Knoten werden in das Modell des JTrees eingefügt.

public void done()  {
    model.removeNodeFromParent( wait );

    java.awt.Component pane = SwingUtilities.getRootPane(tree).getGlassPane();
    pane.setVisible(false);

    try {

	    List<MyNode> children = ( List<MyNode> ) get();
	    for ( Iterator<MyNode> it = children.iterator(); it.hasNext(); ) {
	    	MyNode node = it.next();
	        model.insertNodeInto( node, parent, model.getChildCount( parent ) );
	    }

	} catch (InterruptedException e) {
		e.printStackTrace();
	} catch (ExecutionException e) {
		e.printStackTrace();
	}
}

Damit die beiden eben beschriebenen Methoden verwendet werden können, erstellt man einfach eine Klasse TWEWorker (TreeWillExpandWorker), die von SwingWorker erbt.

private class TWEWorker extends SwingWorker {
    private final DefaultMutableTreeNode parent;
    private final DefaultMutableTreeNode wait;


    TWEWorker( DefaultMutableTreeNode parent ) {
        this.parent = parent;
        wait = new DefaultMutableTreeNode( " Waiting... " );
        parent.add( wait );

        java.awt.Component pane = SwingUtilities.getRootPane(tree).getGlassPane();
        pane.setCursor( Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        pane.setVisible(true);
    }

    ...

 }

Der Konstruktor der Klasse TWEWorker wird aufgerufen, wenn ein Knoten aufgeklappt wird. Im Konstuktor wird als erstes ein Waiting-Note eingefügt und eine GlassPane über das Panel des Baum gelegt.

Die Klasse MyNode

Die Klase MyNode erweitert den DefaultMutualTreeNode. Es gibt zwei Gründe diese Klasse in dem Beispiel zu verwenden. Einmal wird durch die Methode isLeaf() dafür gesorgt, daß sich der JTree unendlich lang aufklappen läßt. Der andere Grund ist, daß der Aufruf von Thread.sleep() benötigt wird, damit das Erstellen der Knoten etwas Zeit verbraucht und die Arbeitsweise des SwingWorkers sichtbar wird. IRONIE: Mit dieser Erweiterung hat man in Kundenprojekten übrigens ein riesiges Verbesserungspotential, wenn der Kunde sich über Performance beklagt. ;-)

private class MyNode extends DefaultMutableTreeNode {
    public MyNode(String s) {
        super(s);
        try {
			Thread.sleep( 100 );
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
    }

    public boolean isLeaf() {
        return false;
    }
}

Der TreeWillExpandListener

Der TreeWillExpandListener wird an JTree registriert, damit beim Aufklappen eines Knotens die Kindknoten eingefügt werden. Ohne SwingWorker würden in der Methode treeWillExpand die neuen DefaultMutableTreeNodes erzeugt und in den Baum eingefügt. Wie aber schon erwähnt, zeichnet sich die Oberfläche in diesem Zeitraum nicht neu. Wenn das Erstellen der Knoten lange dauert, muß deshalb der SwingWorker verwendet werden. Dies ist in der Klasse TWEListener zu sehen.

private class TWEListener implements TreeWillExpandListener {
    public void treeWillCollapse( TreeExpansionEvent expansionEvent ) throws ExpandVetoException {
    }


    public void treeWillExpand(TreeExpansionEvent expansionEvent) throws ExpandVetoException {
        Object lastNode = expansionEvent.getPath().getLastPathComponent();

        // If there are no children then start the SwingWorker
        // to create them.
        if ( model.getChildCount(lastNode) == 0) {
        	SwingWorker worker = new TWEWorker((DefaultMutableTreeNode)lastNode);
            worker.execute();
        }
    }

}

In der Methode treeWillExpand wird zuerst festgestellt, welcher Knoten angeklickt wurde. Für diesen wird überprüft, ob schon Kindknoten existieren. Wenn keine da sind, dann wird der SwingWorker mit der Methode execute gestartet und die Methode wird wieder verlassen.

Die execute-Methode sorgt dafür, daß die Methoden doInBackground() und done() des SwingWorkers zur rechten Zeit aufgerufen werden und die Oberfläche sich wären der Knotenerzeugung neu zeichnet.

Das JPanel

Alle oben angesprochenen Klassen und Methoden werden in dem Beispiel in ein JPanel eingebettet. Im Konstruktor wird der Wurzelknoten und schon mal ein paar Kindknoten erstellt. Außerdem wird der TreeWillExpandListener am JTree angemeldet.

public class JHTreePanel extends JPanel {
    final DefaultTreeModel model;
    final JTree tree;

    public JHTreePanel() {
    	super(new GridLayout(1,0));


        DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Root" );
        model = new DefaultTreeModel( root );

        for ( int i = 0; i < 10; i++ ) {
            DefaultMutableTreeNode child = new MyNode( Integer.toString( i ) );
            root.add( child );
        }

        tree = new JTree( model );
        tree.setShowsRootHandles( true );

        tree.addTreeWillExpandListener( new TWEListener() );

        add(new JScrollPane( tree ));

    }
...
}

Was jetzt noch fehlt ist eine Hauptklasse, die das JPanel verwendet. Bei dieser Klasse handelt es sich um eine Standard-Klasse, die man in vielen Beispielen zu Java findet. Deshalb ist sie hier nicht weiter beschrieben, sondern nur der Vollständigkeit halber aufgeführt.

/**
 * @author hameister
 *
 */
public class MyJTreeExample {

    private static void createAndShowGUI() {

        //Create and set up the window.
        JFrame frame = new JFrame("JHTreeDemo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Add content to the window.
        frame.add(new JHTreePanel());

        frame.setMinimumSize(new Dimension(400, 600));

        //Display the window.
        frame.pack();
        frame.setVisible(true);
    }

	public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

Fazit

Es ist zwar traurig, daß man bei Java immer noch Klimmzüge machen muß, damit sich die Oberfläche neu zeichnet, aber es gibt zumindest eine Möglichkeit ein Neuzeichnen zu erreichen. Mit dem SwingWorker ist es also möglich langlaufende Prozesse, die die Oberfläche beeinflussen, so im Hintergrund laufen zu lassen, daß die Oberfläche weiterhin benutzbar bleibt und auch neu gezeichnet wird.