Swing & Thread (Suite)
Rémi Forax

Plan

EDT et EventQueue

Traitement en tâche de fond

Evènement coalescent

Coalescent Worker

SwingWorker

Appel Symétrique et gestion déportée des évènements

Rappel sur l’EDT

Swing permet à une seul Thread, l'EventDispatchThread de modifier l'interface graphique

L'EDT à pour tâche :

De pomper les évènement de la queue d'évènement

EventQueue

D'appeler les listeners correspondant à l'évènement

EventQueue.dispatchEvent()

D'afficher les composants (PaintEvent)

le traitements en tâche de fond :

EventQueue.invokeAndWait/invokeLater

EventQueue

Queue d’évènement de l’AWT

Gérer par la classe java.awt.EventQueue

getNextEvent()

peekEvent()

souris

clavier

paint

timer

runnable

postEvent()

EventQueue

Obtenir la queue système :
Toolkit.getSystemEventQueue()

Retirer un évènement de la queue (bloquant)
eventQueue.getNextEvent()

Connaître le prochain évènement
eventQueue.peekEvent()

Prochain évènement d'un certain type (eventId)
eventQueue.peekEvent(eventId)

EventQueue

Envoyer un évènement
eventQueue.postEvent(AWTEvent)

Dispatcher l'évènement sur le composant
protected eventQueue.dispatchEvent(AWTEvent)

Il est possible de créer sa propre queue d’évènement et de transférer le contrôle des événements à celle-ci.

Transférer à une nouvelle queue d’évènement
eventQueue.push(EventQueue)

Supprimer une queue d’évènement
(rétablir la queue originale) eventQueue.pop()

Gérer sa propre queue d'évènement

Gestion de la queue d'évènement à la main

public class MyQueue extends EventQueue {

protected void dispatchEvent(AWTEvent event) {

super.dispatchEvent(event);

}

public static void main(String[] args) throws InterruptedException {

Toolkit toolkit=Toolkit.getDefaultToolkit();

EventQueue queue=toolkit.getSystemEventQueue();

MyQueue newQueue=new MyQueue();

queue.push(newQueue); // transfert

JFrame frame=new JFrame("hello");

frame.setSize(400,300);

frame.setVisible(true);

for(;;) {

AWTEvent event=newQueue.getNextEvent(); // bloquant

System.out.println(event);

newQueue.dispatchEvent(event);

}

}

}

Traitement long

Lorsque l’on veux effectuer des traitements en réponse à l’utilisateur, on effectue ces traitements dans un listener donc ceux-ci sont effectués par EventDispatchThread

Comme EDT s’occupe aussi du rafraîchissement de l’interface graphique => Problème

Solution Intermédiaire : Faire les traitements long dans une Thread

Problème : et si le traitement long modifie l’interface graphique

Traitement long (suite)

Problème si le traitement long qui s’exécute dans une Thread veux changer l’interface graphique

Toute la partie SWING n’est pas synchronized

La partie AWT est synchonizé mais risque de deadlock

Solution de l’AWT, utilisé les méthodes EventQueue.invokeAndWait() et EventQueue.invokeLater() pour demander d’exécuter un Runnable par l’EDT

EventQueue.invoke*

Demande à l’EDT d’exécuter une Runnable

Le Runnable est encapsulé dans un événement et posté dans l’EventQueue (eventQueue.postEvent(event))

Lorsque l’on effectue un eventQueue.dispatchEvent() sur cet évènement, la méthode run() du Runnable est appelée

EventQueue.invoke*

Problèmes des méthodes EventQueue.invoke* :

Comme on poste un événement il faut faire attention à ne pas trop en posté pour ne pas noyer les ‘vrais’ évènements
(souris, clavier, rafraîchissements)

Lors du traitement de l’évènement par l’EDT,
celui-ci peut déjà être caduque

Le code est asymétrique (cf plus tard)

Il faut gérer plein d’exceptions
(surtout avec EventQueue.invokeAndWait())

Exemple

On souhaite afficher récursivement la liste de tous les fichiers à partir d’un répertoire
en tâche de fond

static void traverse(final FileListModel model,File file) {

final File[] files=file.listFiles();

if (files==null)

return;

for(File f:files)

if (f.isDirectory())

traverse(model,f);

EventQueue.invokeLater(new Runnable() {

public void run() {

model.addAll(files);

}

});

}

public static void main(String[] args) {

final FileListModel model=new FileListModel();

new Thread() {

@Override public void run() {

traverse(model,new File("/"));

}

}.start();

Exemple

Le modèle de liste de fichiers permet l’insertion
de tous les fichiers d’un répertoire
en une fois.

public class FileListModel extends AbstractListModel {

public int getSize() {

return files.size();

}

public File getElementAt(int index) {

return files.get(index);

}

public void addAll(File... fs) {

int row=files.size();

Collections.addAll(files,fs);

fireIntervalAdded(this,row,files.size()-1);

}

private final ArrayList<File> files=new ArrayList<File>();

}

Evènement coalescents

Rendre les événements coalescents
i.e permettre d’ajouter des valeurs à un Runnable alors que celui-ci est déjà dans la queue d’évènement

Permet d’éviter de générer beaucoup d’évènements

Le Runnable contient des données qui sont « plus à jour »

Runnable coalescent

Le Runnable permet l’ajout de valeurs

public abstract class CoalescentRunnable<V> implements Runnable {

public CoalescentRunnable(Class<V> vtype) {

this.vtype=vtype;

}

public void publish(V... values) {

synchronized(list) {

Collections.addAll(list,values);

}

}

public abstract void run(List<V> values);

@SuppressWarnings("unchecked")

public void run() {

ArrayList<V> values;

synchronized(list) {

values=list;

list=new ArrayList<V>();

}

run(values);

}

private ArrayList<V> list=new ArrayList<V>();

Runnable coalescent

private static void traverse(CoalescentRunnable<File> cr,File dir) {

final File[] files=dir.listFiles();

if (files==null)

return;

cr.publish(files);

EventQueue.invokeLater(cr);

for (File file:files) {

traverse(cr,file);

}

}

public static void main(String[] args) {

final FileListModel model= new FileListModel();

JList list= new JList(model);

final JFrame frame= new JFrame();

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setContentPane(new JScrollPane(list));

frame.setSize(400,300);

frame.setVisible(true);

CoalescentRunnable<File> cr=new CoalescentRunnable<File>(File.class) {

@Override public void run(List<File> files) {

if (!files.isEmpty())

model.addAll(files.toArray(new File[files.size()]));

}

};

traverse(cr,new File("/"));

}

Evènement coalescent

Le Runnable coalescent permet de prendre en compte plus rapidement les valeurs calculées

Mais comme le Runnable coalescent utilise EventQueue.invokeLater(), il y a toujours autant d’évènements postés même si pour les derniers événements le Runnable ne contiendra plus de valeurs.

Solution, ne pas utilisée invokeLater() et

Si il n’y a pas d’évènement correspondant aux valeurs postés un événement

Sinon compléter les valeurs dans l’évènement

Coalescent Worker

public abstract class CoalescentWorker<V> {

public CoalescentWorker(Class<V> vtype) {

this.vtype=vtype;

}

protected abstract void doInBackground(); // appelée par une Thread

protected abstract void process(List<V> values); // appelée par l’EDT

public void execute() {

new Thread() {

@Override public void run() {

doInBackground();

}

}.start();

}

public void publish(V... values) { // appelé par doInBackground

synchronized(list) {

if (!isScheduled) {

isScheduled=true;

Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(event);

}

Collections.addAll(list,values);

}

}

boolean isScheduled=false;

private final CodeEvent event=new CodeEvent();

private final ArrayList<V> list=new ArrayList<V>();

private final Class<V> vtype;

}

Coalescent Worker

On utilise l’interface ActiveEvent qui permet de tagger un événement particulier dans l’EventQueue.

Lors du eventQueue.dispatchEvent() la méthode dispatch() de l’ActiveEvent sera appelée

...

class CodeEvent extends AWTEvent implements ActiveEvent {

public CodeEvent() {

super(CoalescentWorker.this,-1);

}

public void dispatch() {

CoalescentWorker.this.dispatch();

}

}

@SuppressWarnings("unchecked")

void dispatch() {

ArrayList<V> values;

synchronized(list) {

isScheduled=false;

values=list;

list.clear();

}

process(values);

} }

Utilisation du Coalescent Worker

Utilisation pour la liste de fichiers

public static void main(String[] args) {

final FileListModel model=new FileListModel();

JList list=new JList(model);

new CoalescentWorker<File>(File.class) {

@Override protected void doInBackground() {

traverse(model,new File("/"));

}

private void traverse(FileListModel model,File file) {

File[] files=file.listFiles();

if (files==null)

return;

for(File f:files)

if (f.isDirectory())

traverse(model,f);

publish(files);

}

@Override protected void process(List<File> files) {

model.addAll(files.toArray(new File[files.size()]));

}

}.execute();

SwingWorker

javax.swing.SwingWorker (1.6 de Java SE)

Comment ça marche

La thread main appel execute() pour lancé le worker thread et get() pour être blockée sur la réponse (comme les Future)

La worker thread execute la méthode doInBackground() par un executor (ExecutorService). Cette méthode peut appelée les méthodes publish() et/ou setProgress()

L’EDT appel la méthode process() suite à la méthode publish() pour effectuée les changement graphique puis la méthode done() à la fin.

SwingWorker

SwingWorker<T,V> est paramétrée par T le type de retour de doInbackground() et get() et V le type des valeurs appelées entre publish() et process()

Dans doInBackground()

Pour plusieurs appels à publish(), l’EDT sera appelée une fois avec toutes les valeurs des différents publish() (coalescent)

Pour plusieurs appels à setProgress(), les listeners enregistrés addPropertyChangeListener() ne seront appelée qu’avec la dernière valeur au moment de la lecture de l’evènement par l’EDT. (pas de valeur caduque)

Utilisation du SwingWorker

En utilisant le SwingWorker

public static void main(String[] args) throws InterruptedException, ExecutionException {

final FileListModel model=new FileListModel();

SwingWorker<Integer,File> worker=new SwingWorker<Integer,File>() {

@Override protected Integer doInBackground() {

return traverse(model,new File("c:\\"));

}

private int traverse(final FileListModel model,File file) {

final File[] files=file.listFiles();

if (files==null)

return 0;

int sum=0;

for(File f:files)

if (f.isDirectory())

sum+=traverse(model,f);

publish(files);

return sum+files.length;

}

@Override protected void process(File... files) {

model.add(files);

}

};

worker.execute(); // utilise un executor

Utilisation du SwingWorker

Permet d’afficher le nombre de répertoires parcourus
en demandant la valeur renvoyé par doInBackground()

L’appel get() est bloquant tant que la worker thread n’a pas fini d’exécuter doInbackground()

public static void main(String[] args) throws InterruptedException, ExecutionException {

...

JList list=new JList(model);

JFrame frame=new JFrame("Swing Worker Sample");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setContentPane(new JScrollPane(list));

frame.setSize(400,300);

frame.setVisible(true);

System.out.println("files "+worker.get()); // bloquant

}

Code asymétrique

Exemple avec une liste de valeurs calculée par un ExecutorService en tâche de fond

public static void main(String[] args) {

final IntegerListModel model=new IntegerListModel();

JList list=new JList(model);

final ExecutorService service=
Executors.newSingleThreadExecutor();

final JButton button=new JButton("add value");

button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent event) {

...

}

});

JFrame frame=new JFrame("Values - background task");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.getContentPane().add(new JScrollPane(list));

frame.getContentPane().add(button,BorderLayout.SOUTH);

frame.setSize(400,300);

frame.setVisible(true);

}

Code asymétrique

Le code graphique n’est pas dans les mêmes méthodes

button.setEnabled(false);

service.execute(new Runnable() {

public void run() {

try {

Thread.sleep(1);

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

return;

}

final int value=count++;

try {

EventQueue.invokeAndWait(new Runnable() {

public void run() {

model.add(value);

button.setEnabled(true);

}

});

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

} catch (InvocationTargetException e) {

Throwable cause=e.getCause();

if (cause instanceof RuntimeException)

throw (RuntimeException)cause;

if (cause instanceof Error)

throw (Error)cause;

throw new UndeclaredThrowableException(cause);

} } });

Code asymétrique

Le code graphique n’est pas dans les mêmes méthodes

Code compliqué à comprendre

ActionListener

EventQueue

Runnable

Runnable

actionPerformed

run

invokeAndWait

run

Code graphique

Code graphique

Code symétrique

Mettre le code graphique dans une seule méthode, donc dans le listener

Il faut être capable :

d’arrêter le listener tant que le traitement en tâche de fond s’exécute

De traiter les événement comme normalement même si le listener est arrêté

Solution :

Gérer les évènements dans le listener et sortir de la gestion d’évènement lorsque le traitement en tâche de fond est terminé

Gestion déportée des évènements

La gestion des évènements en faite par l’EDT mais pas à l’endroit habituel

Faisable, la gestion des fenêtres AWT modales utilise ce mécanisme

Lors de l’affichage d’une fenêtre modale, le listener est bloqué tant que la fenêtre n’est pas fermée et seul les évènements de la fenêtre sont transmis

Exemple :
JOptionPane.showMessageDialog(frame,"texte");

Gestion déportée des évènements

Le code graphique se situe alors dans la même méthode

ActionListener

EventQueue

Runnable

BackgroundTask

actionPerformed

run

invokeInBackgroundt

Code graphique

Code graphique

Gestion déportée des évènements

Le code graphique se situe alors dans la même méthode

button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent event) {

button.setEnabled(false);

int value;

try {

value = BackgroundEventQueue.invokeInBackground(service,
new Callable<Integer>() {

public Integer call() throws Exception {

Thread.sleep(3000);

return count++;

}

});

} catch(InterruptedException e) {

Thread.currentThread().interrupt();

return;

}

model.add(value);

button.setEnabled(true);

}

int count=1;

});

invokeInBackground()

Squelette de la méthode invokeInBackground()

public class BackgroundEventQueue extends EventQueue {

public static <V> V invokeInBackground(ExecutorService service,

Callable<V> callable) throws InterruptedException {

final BackgroundEventQueue newQueue=new BackgroundEventQueue();

Toolkit toolkit=Toolkit.getDefaultToolkit();

Future<V> future=service.submit(callable);

toolkit.getSystemEventQueue().push(newQueue);

while(...) { // manque la condition d’arrêt

event=newQueue.getNextEvent(); // bloquant

newQueue.dispatchEvent(event);

}

newQueue.pop();

try {

return future.get();

}

catch(ExecutionException e) {

Throwable cause=e.getCause();

if (cause instanceof RuntimeException)

throw (RuntimeException)cause;

if (cause instanceof Error)

throw (Error)cause;

throw new UndeclaredThrowableException(cause);

} }

invokeInBackground()

Il faut débloquer la queue d’évènement lorsque le callable est terminé

Le plus simple consiste à poster un évènement pour débloquer la queue

On utilise un ActiveEvent qui positionne une valeur qui débloquera la boucle d’évènement

...

private static class WakeUpEvent extends AWTEvent implements ActiveEvent {

public WakeUpEvent(Toolkit toolkit) {

super(toolkit,-1); // null is illegal as source

}

public void dispatch() {

wakeUp=true;

}

boolean wakeUp;

}

...

invokeInBackground()

public class BackgroundEventQueue extends EventQueue {

public static <V> V invokeInBackground(ExecutorService service,
final Callable<V> callable) throws InterruptedException {

final BackgroundEventQueue newQueue=new BackgroundEventQueue();

Toolkit toolkit=Toolkit.getDefaultToolkit();

final WakeUpEvent wakeUpEvent=new WakeUpEvent(toolkit);

Future<V> future=service.submit(new Callable<V>() {

public V call() throws Exception {

V value=callable.call();

newQueue.postEvent(wakeUpEvent);

return value;

}

});

toolkit.getSystemEventQueue().push(newQueue);

AWTEvent event;

while(!wakeUpEvent.wakeUp) {

event=newQueue.getNextEvent();

newQueue.dispatchEvent(event);

}

newQueue.pop();

try {

return future.get();

}

catch(ExecutionException e) {

...

}

}

Foxtrot

L’API foxtrot disponible sur le web
http://foxtrot.sourceforge.net/
permet de faire l’équivalent de doInBackground()
avec quelques fonctions plus avancées

2004-11-19T16:43:19