Mieux gérer les interactions avec les composants

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Afin d'améliorer la performance et la réactivité de vos programmes Java, nous allons parler de l'EDT, pour « Event Dispatch Thread ». Comme son nom l'indique, il s'agit d'un thread, d'une pile d'appel. Cependant celui-ci a une particularité, il s'occupe de gérer toutes les modifications portant sur un composant graphique :

  • le redimensionnement ;
  • le changement de couleur ;
  • le changement de valeur ;

Vos applications graphiques seront plus performantes et plus sûres lorsque vous utiliserez ce thread pour effectuer tous les changements qui pourraient intervenir sur votre IHM.

Présentation des protagonistes

Vous savez déjà que, lorsque vous lancez un programme Java en mode console, un thread principal est démarré pour empiler les instructions de votre programme jusqu'à la fin. Ce que vous ignorez peut-être, c'est qu'un autre thread est lancé : celui qui s'occupe de toutes les tâches de fond (lancement de nouveaux threads…).

Or depuis un certain temps, nous ne travaillons plus en mode console mais en mode graphique. Et, je vous le donne en mille, un troisième thread est lancé qui se nomme l'EDT (Event Dispatch Thread). Comme je vous le disais, c'est dans celui-ci que tous les changements portant sur des composants sont exécutés. Voici un petit schéma illustrant mes dires (figure suivante).

Threads lancés au démarrage de tout programme Java

La philosophie de Java est que toute modification apportée à un composant se fait obligatoirement dans l'EDT : lorsque vous utilisez une méthode actionPerformed, celle-ci, son contenu compris, est exécutée dans l'EDT (c'est aussi le cas pour les autres intercepteurs d'événements). La politique de Java est simple : toute action modifiant l'état d'un composant graphique doit se faire dans un seul et unique thread, l'EDT. Vous vous demandez sûrement pourquoi. C'est simple, les composants graphiques ne sont pas « thread-safe » : ils ne peuvent pas être utilisés par plusieurs threads simultanément et assurer un fonctionnement sans erreurs ! Alors, pour s'assurer que les composants sont utilisés au bon endroit, on doit placer toutes les interactions dans l'EDT.

Par contre, cela signifie que si dans une méthode actionPerformed nous avons un traitement assez long, c'est toute notre interface graphique qui sera figée !

Vous vous souvenez de la première fois que nous avons tenté de contrôler notre animation ? Lorsque nous cliquions sur le bouton pour la lancer, notre interface était bloquée étant donné que la méthode contenant une boucle infinie n'était pas dépilée du thread dans lequel elle était lancée. D'ailleurs, si vous vous souvenez bien, le bouton s'affichait comme si on n'avait pas relâché le clic ; c'était dû au fait que l'exécution de notre méthode se faisait dans l'EDT, bloquant ainsi toutes les actions sur nos composants.

Voici un schéma, en figure suivante, résumant la situation.

Pourquoi les IHM Java se figent lors de traitements longs

Imaginez la ligne comme une tête de lecture. Il y a déjà quelques événements à faire dans l'EDT :

  • la création de la fenêtre ;
  • la création et mise à jour de composants ;

Seulement voilà, nous cliquons sur un bouton engendrant un long, un très long traitement dans l'EDT (dernier bloc) : du coup, toute notre IHM est figée ! Non pas parce que Java est lent, mais parce que nous avons exécuté un traitement au mauvais endroit. Il existe toutefois quelques méthodes thread-safe :

  • paint() et repaint() ;
  • validate(), invalidate() et revalidate().

Celles-ci peuvent être appelées depuis n'importe quel thread.

À ce stade, une question se pose : comment exécuter une action dans l'EDT ? C'est exactement ce que nous allons voir.

Utiliser l'EDT

Java vous fournit la classe SwingUtilities qui offre plusieurs méthodes statiques permettant d'insérer du code dans l'EDT :

  • invokeLater(Runnable doRun) : exécute le thread en paramètre dans l'EDT et rend immédiatement la main au thread principal ;
  • invokeAndWait(Runnable doRun) : exécute le thread en paramètre dans l'EDT et attend la fin de celui-ci pour rendre la main au thread principal ;
  • isEventDispatchThread() : retourne vrai si le thread dans lequel se trouve l'instruction est dans l'EDT.

Maintenant que vous savez comment exécuter des instructions dans l'EDT, il nous faut un cas concret :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//CTRL + SHIFT + O pour générer les imports
public class Test1 {
  static int count = 0, count2 = 0;
  static JButton bouton = new JButton("Pause");
  public static void main(String[] args){
    JFrame fen = new JFrame("EDT");      
    fen.getContentPane().add(bouton);
    fen.setSize(200, 100);
    fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    fen.setLocationRelativeTo(null);
    fen.setVisible(true);
    updateBouton();
    System.out.println("Reprise du thread principal");
  }

  public static void updateBouton(){
    for(int i = 0; i < 5; i++){
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      bouton.setText("Pause " + ++count);            
    }
  }   
}

Au lancement de ce test, vous constatez que le thread principal ne reprend la main qu'après la fin de la méthode updateBouton(), comme le montre la figure suivante.

Thread principal bloqué durant un traitement

La solution pour rendre la main au thread principal avant la fin de la méthode, vous la connaissez : créez un nouveau thread, mais cette fois vous allez également exécuter la mise à jour du bouton dans l'EDT. Voilà donc ce que nous obtenons :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//CTRL + SHIFT + O pour générer les imports
public class Test1 {
  static int count = 0;
  static JButton bouton = new JButton("Pause");
  public static void main(String[] args){

    JFrame fen = new JFrame("EDT");      
    fen.getContentPane().add(bouton);
    fen.setSize(200, 100);
    fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    fen.setLocationRelativeTo(null);
    fen.setVisible(true);
    updateBouton();

    System.out.println("Reprise du thread principal");
  }

  public static void updateBouton(){
    //Le second thread
    new Thread(new Runnable(){
      public void run(){
        for(int i = 0; i < 5; i++){
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          //Modification de notre composant dans l'EDT
          Thread t = new Thread(new Runnable(){
            public void run(){
              bouton.setText("Pause " + ++count);
            }
          });
          if(SwingUtilities.isEventDispatchThread())
            t.start();
          else{
            System.out.println("Lancement dans l' EDT");
            SwingUtilities.invokeLater(t);
          }
        }
      }
    }).start();      
  }
}

Le rendu correspond à la figure suivante.

Lancement d'un traitement dans l'EDT

Ce code est rudimentaire, mais il a l'avantage de vous montrer comment utiliser les méthodes présentées. Cependant, pour bien faire, j'aurais aussi dû inclure la création de la fenêtre dans l'EDT, car tout ce qui touche aux composants graphiques doit être mis dans celui-ci.

Pour finir notre tour du sujet, il manque encore la méthode invokeAndWait(). Celle-ci fait la même chose que sa cousine, mais comme je vous le disais, elle bloque le thread courant jusqu'à la fin de son exécution. De plus, elle peut lever deux exceptions : InterruptedException et InvocationTargetException.

Depuis la version 6 de Java, une classe est mise à disposition pour effectuer des traitements lourds et interagir avec l'EDT.

La classe SwingWorker<T, V>

Cette dernière est une classe abstraite permettant de réaliser des traitements en tâche de fond tout en dialoguant avec les composants graphiques via l'EDT, aussi bien en cours de traitement qu'en fin de traitement. Dès que vous aurez un traitement prenant pas mal de temps et devant interagir avec votre IHM, pensez aux SwingWorker.

Vu que cette classe est abstraite, vous allez devoir redéfinir une méthode : doInBackground(). Elle permet de redéfinir ce que doit faire l'objet en tâche de fond. Une fois cette tâche effectuée, la méthode doInBackground() prend fin. Vous avez la possibilité de redéfinir la méthode done(), qui a pour rôle d'interagir avec votre IHM tout en s'assurant que ce sera fait dans l'EDT. Implémenter la méthode done() est optionnel, vous n'êtes nullement tenus de le faire.

Voici un exemple d'utilisation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//CTRL + SHIFT + O pour générer les imports
public class Test1 {
  static int count = 0;
  static JButton bouton = new JButton("Pause");
  public static void main(String[] args){

    JFrame fen = new JFrame("EDT");      
    fen.getContentPane().add(bouton);
    fen.setSize(200, 100);
    fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    fen.setLocationRelativeTo(null);
    fen.setVisible(true);
    updateBouton();

    System.out.println("Reprise du thread principal");
  }

  public static void updateBouton(){
    //On crée le SwingWorker   
    SwingWorker sw = new SwingWorker(){
      protected Object doInBackground() throws Exception {
        for(int i = 0; i < 5; i++){
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }               
        }
        return null;
      }

      public void done(){            
        if(SwingUtilities.isEventDispatchThread())
          System.out.println("Dans l'EDT ! ");
        bouton.setText("Traitement terminé");
      }         
    };
    //On lance le SwingWorker
    sw.execute();
  }
}

Vous constatez que le traitement se fait bien en tâche de fond, et que votre composant est mis à jour dans l'EDT. La preuve à la figure suivante.

Utilisation d'un objet SwingWorker

Je vous disais plus haut que vous pouviez interagir avec l'EDT pendant le traitement. Pour ce faire, il suffit d'utiliser la méthode setProgress(int progress) combinée avec l'événement PropertyChangeListener, qui sera informé du changement d'état de la propriété progress.

Voici un code d'exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//CTRL + SHIFT + O pour générer les imports
public class Test1 {
  static int count = 0;
  static JButton bouton = new JButton("Pause");
  public static void main(String[] args){

    JFrame fen = new JFrame("EDT");      
    fen.getContentPane().add(bouton);
    fen.setSize(200, 100);
    fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    fen.setLocationRelativeTo(null);
    fen.setVisible(true);
    updateBouton();

    System.out.println("Reprise du thread principal");
  }

  public static void updateBouton(){
    SwingWorker sw = new SwingWorker(){
      protected Object doInBackground() throws Exception {
        for(int i = 0; i < 5; i++){
          try {
            //On change la propriété d'état
            setProgress(i);
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }               
        }
        return null;
      }

      public void done(){
        if(SwingUtilities.isEventDispatchThread())
          System.out.println("Dans l'EDT ! ");
        bouton.setText("Traitement terminé");
      }         
    };
    //On écoute le changement de valeur pour la propriété
    sw.addPropertyChangeListener(new PropertyChangeListener(){
      //Méthode de l'interface
      public void propertyChange(PropertyChangeEvent event) {
        //On vérifie tout de même le nom de la propriété
        if("progress".equals(event.getPropertyName())){
          if(SwingUtilities.isEventDispatchThread())
            System.out.println("Dans le listener donc dans l'EDT ! ");
          //On récupère sa nouvelle valeur
          bouton.setText("Pause " + (Integer) event.getNewValue());
        }            
      }         
    });
    //On lance le SwingWorker
    sw.execute();
  }
}

La figure suivante présente le résultat du code.

Utilisation de setProgress(int i)

Les méthodes que vous avez vues jusqu'ici sont issues de la classe SwingWorker, qui implémente l'interface java.util.concurrent.Future, offrant les méthodes suivantes :

  • get() : permet à la méthode doInBackground() de renvoyer son résultat à d'autres threads ;
  • cancel() : essaie d'interrompre la tâche de doInBackground() en cours ;
  • isCancelled() : retourne vrai si l'action a été interrompue ;
  • isDone() : retourne vrai si l'action est terminée

Nous pouvons donc utiliser ces méthodes dans notre objet SwingWorker afin de récupérer le résultat d'un traitement. Pour le moment, nous n'avons pas utilisé la généricité de cette classe. Or, comme l'indique le titre de cette section, SwingWorker peut prendre deux types génériques. Le premier correspond au type de renvoi de la méthode doInBackground() et, par extension, au type de renvoi de la méthode get(). Le deuxième est utilisé comme type de retour intermédiaire pendant l'exécution de la méthode doInBackground().

Afin de gérer les résultats intermédiaires, vous pouvez utiliser les méthodes suivantes :

  • publish(V value) : publie le résultat intermédiaire pour la méthode progress(List<V> list) ;
  • progress(List<V> list) : permet d'utiliser le résultat intermédiaire pour un traitement spécifique.

Voici l'exemple utilisé jusqu'ici avec les compléments :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//CTRL + SHIFT + O pour générer les imports
public class Test1 {
  static int count = 0;
  static JButton bouton = new JButton("Pause");
  public static void main(String[] args){

    JFrame fen = new JFrame("EDT");      
    fen.getContentPane().add(bouton);
    fen.setSize(200, 100);
    fen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    fen.setLocationRelativeTo(null);
    fen.setVisible(true);
    updateBouton();

    System.out.println("Reprise du thread principal");
  }

  public static void updateBouton(){
    //On crée un Worker générique, cette fois
    SwingWorker sw = new SwingWorker<Integer, String>(){
      protected Integer doInBackground() throws Exception {
        int i;
        for(i = 0; i < 5; i++){
          try {
            //On change la propriété d'état
            setProgress(i);
            //On publie un résultat intermédiaire 
            publish("Tour de boucle N° " + (i+1));
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }               
        }
        return i;
      }

      public void done(){
        if(SwingUtilities.isEventDispatchThread())
          System.out.println("Dans l'EDT ! ");
        try {
          //On utilise la méthode get() pour récupérer le résultat
          //de la méthode doInBackground()
          bouton.setText("Traitement terminé au bout de "+get()+" fois !");
        } catch (InterruptedException e) {
          e.printStackTrace();
        } catch (ExecutionException e) {
          e.printStackTrace();
        }
      }   
      //La méthode gérant les résultats intermédiaires
      public void process(List<String> list){
        for(String str : list)
          System.out.println(str);
      }
    };
    //On écoute le changement de valeur pour la propriété
    sw.addPropertyChangeListener(new PropertyChangeListener(){
      //Méthode de l'interface
      public void propertyChange(PropertyChangeEvent event) {
        //On vérifie tout de même le nom de la propriété
        if("progress".equals(event.getPropertyName())){
          if(SwingUtilities.isEventDispatchThread())
            System.out.println("Dans le listener donc dans l'EDT ! ");
          //On récupère sa nouvelle valeur
          bouton.setText("Pause " + (Integer) event.getNewValue());
        }            
      }         
    });
    //On lance le SwingWorker
    sw.execute();
  }
}

Et le résultat, à la figure suivante, parle de lui-même.

Utilisation de types génériques avec un objet SwingWorker

Voilà : vous savez maintenant comment utiliser l'EDT et les SwingWorker. Vos applications n'en seront que plus réactives !


  • Au lancement d'un programme Java, trois threads se lancent : le thread principal, celui gérant les tâches de fond et l'EDT.
  • Java préconise que toute modification des composants graphiques se fasse dans l'EDT.
  • Si vos IHM se figent, c'est peut-être parce que vous avez lancé un traitement long dans l'EDT.
  • Afin d'améliorer la réactivité de vos applications, vous devez choisir au mieux dans quel thread vous allez traiter vos données.
  • Java offre la classe SwingUtilities, qui permet de lancer des actions dans l'EDT depuis n'importe quel thread.
  • Depuis Java 6, la classe SwingWorker(<T, V>) vous offre la possibilité de lancer des traitements dans un thread en vous assurant que les mises à jour des composants se feront dans l'EDT.