SwingWorker (Java SE 6)

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Java SE 6 offre une version totalement refaite de la classe SwingWorker. Cette classe permet de faciliter les interactions entre un thread utilisateur et l'EventDispatchThread. Pour utiliser la classe SwingWorker, il faut tout d'abord comprendre les problèmes et les motivations d'une telle abstraction. Ce tutoriel va tenter d'expliquer le but de cette classe, et de présenter un exemple d'utilisation.

II. L'EventDispatchThread

Lorsque vous démarrez un programme java, 3 threads sont démarrés :
  • le thread principal qui exécute la méthode main ;
  • le thread de gestion mémoire (garbage collector) ;
  • l'EventDispatchThread.

L'EventDispatchThread est le thread qui s'occupe de l'interface graphique, c'est-à-dire l'affichage des fenêtres, les actions à exécuter lors des clics sur les boutons, etc... Le principe est simple : tout ce qui concerne l'interface graphique doit être exécuté dans ce thread, de manière séquentielle. Ceci a un gros avantage : il n'y a pas besoin de synchronisation (en savoir plus sur la synchronisation en java), puisque les actions de l'interface graphique ne sont pas exécutées en parallèle.

Mais bien sûr, parfois, à partir d'un thread utilisateur, nous avons besoin de modifier des éléments graphiques (mise à jour de textfields, désactivation d'un bouton...). Dans ce cas, il faut utiliser la méthode static SwingUtilities#invokeLater(Runnable), où Runnable contient les actions à effectuer dans l'EDT.

SwingUtilities#invokeLater(Runnable)

Comme vous pouvez le voir sur le schéma, le Runnable s'exécutera de manière asynchrone par rapport au thread utilisateur (d'où le nom de la méthode invokeLater). Les curseurs rouges représentent l'exécution courante dans chaque thread. Aussitôt après l'appel à invokeLater dans le thread utilisateur, le code continue de s'exécuter, alors que le Runnable n'est peut-être même pas commencé. Pour attendre la terminaison de l'exécution du Runnable avant de continuer, il faut utiliser SwingUtilities#invokeAndWait(Runnable) (mais dans la majorité des cas, on peut s'en passer).

Certaines méthodes de l'API Swing sont thread-safe, c'est-à-dire que la contrainte d'exécuter cette méthode dans l'EDT n'est pas violée si vous l'appelez depuis un thread utilisateur : c'est la méthode qui se charge de l'exécution dans l'EDT. Cette particularité est précisée dans la javadoc de ces méthodes, mais de toute façon, vous ne perdrez rien à les exécuter dans l'EDT.

Les actions effectuées par l'EDT ne sont pas forcément fournies par l'utilisateur par le biais d'un Runnable. Les méthodes des listeners de Swing sont également appelées dans l'EDT, tout comme tout ce qui concerne l'affichage des fenêtres et son rafraîchissement. Or, si sur le schéma ci-dessus l'action 4 correspond à la mise à jour de l'affichage de la fenêtre, et que l'action 3 représente un calcul très coûteux en temps, l'interface va se figer (en savoir plus sur ce comportement) et vous aurez l'impression que votre ordinateur rame (alors que c'est simplement dû à une mauvaise programmation). C'est pourquoi il est nécessaire d'effectuer des calculs dans des threads utilisateurs, mais tout en respectant la contrainte que l'affichage doit être exécutée dans l'EDT.

Bien sûr, nous pourrions créer un Thread qui effectue les calculs, et qui périodiquement, au fur et à mesure du calcul, met à jour l'interface graphique en appelant invokeLater. C'est faisable, mais c'est justement ce que la classe SwingWorker permet de simplifier.

III. Présentation de SwingWorker

SwingWorker est une classe abstraite, possédant 2 types paramétrés (en savoir plus sur la généricité), qui seront décrits lors de la présentation des méthodes les utilisant.

Voici une présentation des méthodes principales à utiliser. (Vous pouvez également consulter la javadoc pour plus d'informations.)

Méthode Description
protected abstract T doInBackground() à redéfinir. La seule méthode abstraite. C'est dans cette méthode qu'il faut définir le code à exécuter dans un thread séparé (un calcul long par exemple). Cette méthode retourne un résultat, du type T (type passé en paramètre de la classe), que l'on peut récupérer grâce à la méthode get() une fois le traitement terminé.
protected void done() à redéfinir (éventuellement). Permet d'effectuer des actions dans l'EDT une fois que le traitement (effectué par doInBackground) est terminé.
protected final void publish(V... chunks) Permet de transmettre des résultats partiels du traitement. Le paramètre utilise l'ellipse, permettant d'avoir un nombre quelconque d'arguments (en savoir plus sur l'ellipse), ici de type V. V est le second type passé en paramètre de la classe, et définit le type des résultats partiels à transmettre à la méthode process.
protected void process(List<V> chunks) à redéfinir (éventuellement). Permet de récupérer les résultats partiels du traitement dans l'EDT publiés par la méthode publish. Cette méthode prend en paramètre une liste de V, et non uniquement un V, car pour des raisons d'efficacité, plusieurs appels à publish peuvent résulter en 1 seul appel à process, en transmettant donc plusieurs résultats à la fois.
public final T get() Permet de récupérer le résultat renvoyé par doInBackground, en attendant éventuellement que le traitement se termine s'il n'est pas terminé. Cette méthode étant bloquante, il faut donc éviter de l'appeler à partir de l'EDT (sauf si vous savez ce que vous faites).
protected void setProgress(int progress) Indique le nouvel état d'avancement du traitement (à appeler donc dans la méthode doInBackground). Comme nous le verrons plus tard, ceci est bien pratique pour mettre à jour une barre de progression.
public final void addPropertyChangeListener (PropertyChangeListener listener) Ajoute un écouteur de propriétés, permettant notamment d'écouter l'avancement du traitement, provoqué par setProgress.
public final void execute() Démarre l'exécution de doInBackground dans un thread séparé.

IV. Exemple d'utilisation

Pour mieux comprendre comment fonctionne SwingWorker, étudions un exemple. Nous voulons compter combien de fichiers se trouvent dans le répertoire utilisateur, en comptant récursivement ceux des sous-répertoires. Pendant le calcul (qui peut être long), nous voulons afficher les fichiers en cours de parcours. A la fin du calcul, nous voulons afficher le nombre de fichiers ainsi comptés.

Voici un code source utilisant SwingWorker qui permet de faire cela. Vous pouvez le copier-coller puis le compiler (j'ai volontairement tout écrit dans un seul fichier, pour des raisons pratiques).

 
Sélectionnez

import java.awt.BorderLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.List;
 
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
 
public class SwingWorkerDemo extends JFrame {
 
	private JTextArea textArea;
	private JTextField textField;
	private JProgressBar progressBar;
 
	class MonSwingWorker extends SwingWorker<Integer, String> {
 
		public MonSwingWorker() {
			/* On ajoute un écouteur de barre de progression. */
			addPropertyChangeListener(new PropertyChangeListener() {
 
				public void propertyChange(PropertyChangeEvent evt) {
					if ("progress".equals(evt.getPropertyName())) {
						progressBar.setValue((Integer) evt.getNewValue());
					}
				}
			});
		}
 
		@Override
		public Integer doInBackground() {
			File userDir = new File(System.getProperty("user.dir"));
			return getNombreDeFichiers(userDir, 0, 100);
		}
 
		/* Compte le nombre de fichiers du répertoire utilisateur. */
		private int getNombreDeFichiers(File dir, double progressStart, double progressEnd) {
			File[] files = dir.listFiles();
			int nb = 0;
			if (files.length > 0) {
				/* Le calcul de l'avancement du traitement n'a que peu d'importance pour l'exemple. */
				double step = (progressEnd - progressStart) / files.length;
 
				for (int i = 0; i < files.length; i++) {
					File f = files[i];
					double progress = progressStart + i * step;
 
					/* Transmet la nouvelle progression. */
					setProgress(Math.min((int) progress, 100));
 
					/*
					 * Ajout d'un temps d'attente pour observer les changements à l'échelle
					 * "humaine".
					 */
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
 
					if (f.isDirectory()) {
						/* Publication du répertoire trouvé. */
						publish("Exploration du répertoire " + f.getAbsolutePath() + "...");
						nb += getNombreDeFichiers(f, progress, progress + step);
					} else {
						/* Publication du fichier trouvé. */
						publish(f.getAbsolutePath());
						nb++;
					}
				}
			}
			return nb;
		}
 
		@Override
		protected void process(List<String> strings) {
			/* Affichage des publications reçues dans le textarea. */
			for (String s : strings)
				textArea.append(s + '\n');
		}
 
		@Override
		protected void done() {
			try {
				/* Le traitement est terminé. */
				setProgress(100);
				/*
				 * À la fin du traitement, affichage du nombre de fichiers parcourus dans le
				 * textfield.
				 */
				textField.setText(String.valueOf(get()));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
 
	public SwingWorkerDemo() {
		/* Construction de l'interface graphique. */
		super("SwingWorkerDemo");
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		textArea = new JTextArea(12, 40);
		textArea.setEnabled(false);
		textField = new JTextField(5);
		textField.setEnabled(false);
		progressBar = new JProgressBar();
		JPanel content = new JPanel(new BorderLayout());
		content.add(new JScrollPane(textArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
				ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
		JPanel south = new JPanel(new BorderLayout());
		south.add(progressBar);
		south.add(textField, BorderLayout.EAST);
		content.add(south, BorderLayout.SOUTH);
		setContentPane(content);
		pack();
		setLocation(100, 100);
		setVisible(true);
	}
 
	public static void main(String... args) {
		SwingUtilities.invokeLater(new Runnable() {
 
			public void run() {
				/* Démarrage de l'interface graphique et du SwingWorker. */
				SwingWorkerDemo demo = new SwingWorkerDemo();
				MonSwingWorker swingWorker = demo.new MonSwingWorker();
				swingWorker.execute();
			}
		});
	}
 
}

Ce code utilise toutes les méthodes décrites précédemment, ce qui permet d'observer leur utilisation.

SwingWorker ne garantit pas que la méthode done sera appelée après tous les appels à process. Ainsi, si vous mettez dans la méthode done l'affichage d'une ligne supplémentaire dans le textarea, cette ligne ne se trouvera pas forcément en dernière dans le textarea.

V. Conclusion

SwingWorker permet de faciliter la transmission de messages entre un thread de calcul et l'EDT. Il suffit de définir la méthode qui fait le calcul en arrière-plan, de décrire les messages à transmettre à l'EDT et de définir les actions à effectuer dans l'EDT à la réception de ces messages. De plus, il est aisé de gérer une barre de progression, même si le calcul de l'avancement n'est pas toujours trivial, et est forcément erroné dans certains cas (par exemple, ici, on ne peut pas savoir l'avancement exact, puisqu'on ne connait pas le nombre total de fichiers - c'est ce que l'on cherche à calculer).

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.