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, trois 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.
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 deux 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 un 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. À 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).
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).