Faire des interfaces graphiques maintenables en Swing

Swing a pendant longtemps été le toolkit graphique de référence dans le monde Java. Ce n’est toutefois plus le cas depuis Java 7, étant donné qu’il a été remplacé par JavaFX 2 (à ne pas confondre avec JavaFX qui prétendait être un concurrent de Silverlight, les deux sont morts et enterrés). Néanmoins il est encore suffisamment répandu pour qu’on en parle ici.

 

Quelques rappels sur Swing

Swing est un toolkit graphique qui a été introduit avec Java 2, oui ça date, pour remplacer AWT. Contrairement à ce dernier Swing ne s’appuie pas directement sur les widgets natifs du système, mais au contraire génère une image de l’interface, et ensuite suivant l’endroit où l’utilisateur a cliqué sur l’image il redirige le clic vers le bon composant graphique pour que ce dernier effectue l’opération demandée.

Swing s’appuie entièrement sur le design pattern composite, tel qu’affiché ci-dessous. Pour rappel il s’agit d’une structure de données récursive, dans laquelle chaque composant peut contenir des enfants, soit de type composant, soit des feuilles. Un des intérêt est que si on lance une opération sur un composant parent, celle-ci sera transmise à tous ses enfants et ainsi de suite.

Le design pattern MVC

Une autre règle de base quand on utilise des interfaces graphiques est de modéliser son application en suivant le design pattern Modèle-Vue-Contrôleur, ou encore MVC. Dans ce design pattern l’utilisateur fait une action en appelant le contrôleur. Celui-ci appelle à son tour le modèle, et ce dernier notifie la vue qu’elle doit se rafraîchir en interrogeant le modèle. La figure ci-dessous représente ce design-pattern :

Typiquement le lien entre le modèle et la vue est implémenté avec le design pattern Observer, tel que représenté ci-dessous. Dedans, pour rappel, la vue s’enregistre auprès du modèle, et quand ce dernier est mis à jour il notifie la vue qui interroge le modèle.

L’intérêt d’utiliser une telle organisation est qu’on s’assure ainsi que le modèle est bien réutilisable car il est faiblement couplé à la vue. Ce point est capital car le modèle contient généralement le code métier de l’application. Or on pourrait typiquement vouloir intégrer ce modèle dans une application web, une application en ligne de commande et enfin l’application graphique, et ce sans avoir besoin de tout réécrire.

Il ne faut par ailleurs jamais perdre de vue que l’interface graphique est ce qui bouge le plus dans une application. En d’autres termes celle-ci doit quasiment être considérée comme du code jetable, contrairement au modèle. D’où l’importance de toujours maintenir un couplage faible du modèle vers la vue.

 

Faire des composants réutilisables

Un détail qui a toute son importance sur Swing est qu’il implémente en interne le modèle MVC. En effet de nombreux composants ont besoin d’un état interne pour pouvoir s’afficher correctement. Par exemple on veut pouvoir faire en sorte que tel ou tel composant s’affiche en rouge en fonction de telle condition.

Je ne sais si vous avez remarqué mais à cet effet Swing repose sur un modèle de listeners, tout comme d’autres frameworks graphiques comme JavaFX 2 (tiens on en reparle de celui-là. 😉 ) L’idée est que les autres composants s’abonnent à ces listeners et réagissent en fonction. Là où ça devient intéressant est que pour vos propres composants vous devriez adopter un tel mécanisme.

Pour implémenter ce mécanisme, voici la procédure. Créez tout d’abord un objet contenant les informations à transporter entre l’objet et sa cible, ici on va l’appeler MonEvent :


public class MonEvent {

   private String msg;

   public MonEvent(final String msg) {
      this.msg = msg;
   }

   public String getMsg() {
      return msg;
   }

}

Dans l’idéal l’objet MonEvent doit être immutable.

L’étape suivante consiste à définir une interface que tous les objets qui veulent s’abonner à MonEvent devront implémenter. On va l’appeler MonEventListener :


public interface MonEventListener extends EventListener {

   public void actionPerformed(MonEvent e);
}

Vous noterez que l’interface MonEventListener hérite de l’interface Swing EventListener. Cette dernière joue le rôle de marqueur et pour bénéficier entièrement du framework Swing il est fortement conseillé d’en hériter !

Enfin, on définit la classe MonComposant, sur laquelle des listeners peuvent s’abonner :


public class MonComponent extends JPanel {

   // ...

   private EventListenerList listeners;

   public MonComponent(...) {
      // ...
      this.listeners = new EventListenerList();
      // ...
   }

   public addMonEventListener(final MonEventListener listener) {
      this.listeners.add(MonEventListener.class, listener);
   }

   public removeMonEventListener(final MonEventListener listener) {
      this.listeners.remove(MonEventListener.class, listener);
   }

   protected void fireMonEvent(final String msg) {
      for (MonEventListener listener: listeners.get(MonEventListener.class)) {
         listener.actionPerformed(new MonEvent(msg));
      }
   }
}

On peut noter plusieurs choses dans ce code :

  • Tout d’abord les listeners sont stockés dans une instance d’EventListenerList. Il s’agit d’une sorte de Map thread safe, qui à une classe de listeners associe la liste des listeners associés. Et comme vous le voyez dans la méthode fireMonEvent on récupère facilement tous les listeners qui nous intéressent.
  • Ensuite si vous voulez notifier tous vos listeners d’un événement, il suffit d’invoquer la méthode fireMonEvent. Le nom fireXXX n’est ici pas obligatoire, néanmoins il s’agit d’une convention Swing qu’il est préférable de respecter pour des raisons de lisibilité.

Bien évidemment on peut attacher plusieurs types de listeners à un objet donné. 😉

Un exemple concret sur les listeners

Comme tout ça peut paraître bien abstrait, imaginons par exemple que vous deviez créer un composant contenant deux boutons. L’un d’entre eux ne fait qu’actualiser son affichage, tandis que l’autre fait une opération qui impacte d’autres composants.

La première solution pour implémenter ceci serait que le composant en question connaisse chacun des autres composants à rafraîchir, en dur, et appelle la méthode qui convient sur chacun d’entre eux. C’est lourd, et surtout ce n’est pas réutilisable. Si un jour vous ajoutez un autre composant à notifier, vous devez modifier le code de votre composant de base.

L’autre solution consiste justement à ce que les autres composants s’enregistrent directement en tant que listeners du composant de base qui contient les deux boutons. Et quand l’utilisateur clique sur le bouton qui doit notifier les autres composants, notre composant ne fait qu’envoyer un message aux listeners. Ainsi notre composant de base devient réutilisable.

Alors vous me direz, mais comment faire alors pour attacher tous ces composants entre eux ? La réponse est en fait simple : dans votre application vous aurez une ou plusieurs fenêtres parentes. Celles-ci vont en fait se comporter comme des médiateurs, en reliant les composants les uns aux autres comme on le souhaite. Et pour rappel le pattern médiateur n’est qu’un super observer qui fait du routage entre les objets observés et les observateurs.
 

Swing et le multithread

Il est nécessaire de lancer les traitements longs dans des threads annexes, sinon l’utilisateur verra de temps à autres l’interface geler avec des effets moches. Malheureusement, comme beaucoup de frameworks graphiques, Swing n’est pas thread-safe. D’ailleurs c’est aussi le cas pour JavaFX 2. L’explication est que ça pourrait amener à des deadlocks, pour plus de détails vous pouvez regarder ici.

Pour faire simple sauf exception expresse de la Javadoc, tout appel à des composants Swing doit se faire depuis l’Event Dispatch Thread, qui est le thread de rafraîchissement des composants Swing. Si vous ne respectez pas cette règle, vous risquez tout simplement de corrompre votre affichage, ou d’avoir des exceptions bizarres et non reproductibles.

En d’autres termes quand vous faites un appel à Swing, utilisez toujours l’API suivante :


SwingUtilities.invokeLater(java.lang.Runnable);

Par ailleurs il existe un autre piège assez désagréable qui peut se produire quand vous faites ça, si vous passez à votre objet Swing d’autres objets qui viennent d’un autre thread, à savoir que l’état de ces objets peut très bien avoir changé entre le moment où vous faites l’appel à SwingUtilities.invokeLater et celui où le Runnable est effectivement appelé. Ca m’est arrivé sur mon jeu de Puissance 4, et j’ai passé plusieurs semaines à le déboguer.

Pour vous protéger de ça il n’y qu’une seule solution : faites dans un bloc synchronized une copie de l’objet à passer au composant Swing, de telle façon que la copie en question ne soit pas altérée entre le moment où votre objet est notifié et celui où le Runnable passé à SwingUtilities.invokeLater est effectivement exécuté.

A noter aussi que depuis Java 6 la classe SwingWorker permet de se simplifier la vie.

En bref

Swing offre un modèle très puissant de composants, lequel a d’ailleurs en grande partie été repris dans JavaFX 2. Encore faut-il le maîtriser, et à ce niveau c’est déjà plus compliqué.

L’autre point à ne jamais négliger est que Swing comme de nombreux frameworks graphiques n’est pas thread-safe. Ne pas respecter ce point peut mener à des résultats assez… désagréables.
 

Cet article vous a plu ? Vous aimerez sûrement aussi :

Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.

Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.

Son Twitter Son LinkedIn

gojul

Recent Posts

MICI au travail : Le handicap invisible qui révèle des forces insoupçonnées

Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…

2 jours ago

Exploiter les NPUs pour de l’IA embarquée dans les applis webs

Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…

6 jours ago

Qcm saison hiver 2024 : toutes les infos.

On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…

3 semaines ago

L’inclusion numérique est essentielle.

Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…

4 semaines ago

Communauté Tech et féminine : Interview avec Helvira de Motiv’her

Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…

1 mois ago