Breadcrumb
Last updated on Tuesday, September 13, 2016
  • 40 hours
  • Hard

Free online content available in this course.

Paperback available in this course

eBook available in this course.

Certificate of achievement available at the end this course

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Interagir avec des boutons

Log in or subscribe to enjoy all this course has to offer!

Nous avons vu dans le chapitre précédent les différentes façons de positionner des boutons et, par extension, des composants (car oui, ce que nous venons d'apprendre pourra être réutilisé avec tous les autres composants que nous verrons par la suite).

Maintenant que vous savez positionner des composants, il est grand temps de leur indiquer ce qu'ils doivent faire. C'est ce que je vous propose d'aborder dans ce chapitre. Mais avant cela, nous allons voir comment personnaliser un bouton. Toujours prêts ?

Une classe Bouton personnalisée

Créons une classe héritant dejavax.swing.JButtonque nous appelleronsBoutonet redéfinissons sa méthodepaintComponent(). Vous devriez y arriver tout seuls. Cet exemple est représenté à la figure suivante :

Bouton personnalisé
Bouton personnalisé

Voici la classeBoutonde cette application :

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
 
import javax.swing.JButton;
  
public class Bouton extends JButton {
  private String name;
  public Bouton(String str){
    super(str);
    this.name = str;
  }
        
  public void paintComponent(Graphics g){
    Graphics2D g2d = (Graphics2D)g;
    GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
    g2d.setPaint(gp);
    g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
    g2d.setColor(Color.white);
    g2d.drawString(this.name, this.getWidth() / 2 - (this.getWidth()/ 2 /4), (this.getHeight() / 2) + 5);
  }        
}

J'ai aussi créé un bouton personnalisé avec une image de fond, comme le montre la figure suivante.

Image de fond du bouton
Image de fond du bouton

Voyez le résultat en figure suivante.

Bouton avec une image de fond
Bouton avec une image de fond

J'ai appliqué l'image (bien sûr, ladite image se trouve à la racine de mon projet !) sur l'intégralité du fond, comme je l'ai montré lorsque nous nous amusions avec notrePanneau. Voici le code de cette classeBouton :

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
  
public class Bouton extends JButton{
  private String name;
  private Image img;

  public Bouton(String str){
    super(str);
    this.name = str;
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void paintComponent(Graphics g){
    Graphics2D g2d = (Graphics2D)g;
    GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
    g2d.setPaint(gp);
    g2d.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
    g2d.setColor(Color.black);
    g2d.drawString(this.name, this.getWidth() / 2 - (this.getWidth() / 2 /4), (this.getHeight() / 2) + 5);
  }
}

Rien de compliqué jusque-là… C'est à partir de maintenant que les choses deviennent intéressantes !

Et si je vous proposais de changer l'aspect de votre objet lorsque vous cliquez dessus avec votre souris et lorsque vous relâchez le clic ? Il existe des interfaces à implémenter qui permettent de gérer toutes sortes d'événements dans votre IHM. Le principe est un peu déroutant au premier abord, mais il est assez simple lorsqu'on a un peu pratiqué. N'attendons plus et voyons cela de plus près !

Interactions avec la souris : l'interfaceMouseListener

Avant de nous lancer dans l'implémentation, vous pouvez voir le résultat que nous allons obtenir sur les deux figures suivantes.

Apparence du bouton au survol de la souris
Apparence du bouton au survol de la souris
Apparence du bouton lors d'un clic de souris
Apparence du bouton lors d'un clic de souris

Il va tout de même falloir passer par un peu de théorie avant d'arriver à ce résultat. Pour détecter les événements qui surviennent sur votre composant, Java utilise ce qu'on appelle le design pattern observer. Je ne vous l'expliquerai pas dans le détail tout de suite, nous le verrons à la fin de ce chapitre.

Vous vous en doutez, nous devrons implémenter l'interfaceMouseListenerdans notre classeBouton. Nous devrons aussi préciser à notre classe qu'elle devra tenir quelqu'un au courant de ses changements d'état par rapport à la souris. Ce quelqu'un n'est autre… qu'elle-même ! Eh oui : notre classe va s'écouter, ce qui signifie que dès que notre objet observable (notre bouton) obtiendra des informations concernant les actions effectuées par la souris, il indiquera à l'objet qui l'observe (c'est-à-dire à lui-même) ce qu'il doit effectuer.

Cela est réalisable grâce à la méthodeaddMouseListener(MouseListener obj)qui prend un objetMouseListeneren paramètre (ici, elle prendrathis). Rappelez-vous que vous pouvez utiliser le type d'une interface comme supertype : ici, notre classe implémente l'interfaceMouseListener, nous pouvons donc utiliser cet objet comme référence de cette interface.

Voici à présent notre classeBouton :

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException; 
import javax.imageio.ImageIO;
import javax.swing.JButton;

public class Bouton extends JButton implements MouseListener{
  private String name;
  private Image img;
  public Bouton(String str){
    super(str);
    this.name = str;
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
    //Grâce à cette instruction, notre objet va s'écouter
    //Dès qu'un événement de la souris sera intercepté, il en sera averti
    this.addMouseListener(this);
  }

  public void paintComponent(Graphics g){
    Graphics2D g2d = (Graphics2D)g;
    GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
    g2d.setPaint(gp);
    g2d.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
    g2d.setColor(Color.black);
    g2d.drawString(this.name, this.getWidth() / 2 - (this.getWidth() /  2 /4), (this.getHeight() / 2) + 5);
  }

  //Méthode appelée lors du clic de souris
  public void mouseClicked(MouseEvent event) { }

  //Méthode appelée lors du survol de la souris
  public void mouseEntered(MouseEvent event) { }

  //Méthode appelée lorsque la souris sort de la zone du bouton
  public void mouseExited(MouseEvent event) { }

  //Méthode appelée lorsque l'on presse le bouton gauche de la souris
  public void mousePressed(MouseEvent event) { }

  //Méthode appelée lorsque l'on relâche le clic de souris
  public void mouseReleased(MouseEvent event) { }       
}

C'est en redéfinissant ces différentes méthodes présentes dans l'interfaceMouseListenerque nous allons gérer les différentes images à dessiner dans notre objet.
Rappelez-vous en outre que même si vous n'utilisez pas toutes les méthodes d'une interface, vous devez malgré tout insérer le squelette des méthodes non utilisées (avec les accolades), cela étant également valable pour les classes abstraites.

Nous n'avons alors plus qu'à modifier notre image en fonction de la méthode invoquée. Notre objet comportera les caractéristiques suivantes :

  • il aura une teinte jaune au survol de la souris ;

  • il aura une teinte orangée lorsque l'on pressera le bouton gauche ;

  • il reviendra à la normale si on relâche le clic.

Pour ce faire, je vous propose de télécharger les fichiers PNG dont je me suis servi (rien ne vous empêche de les créer vous-mêmes).

Télécharger les images

Voici maintenant le code de notre classeBoutonpersonnalisée :

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException; 
import javax.imageio.ImageIO;
import javax.swing.JButton;
  
public class Bouton extends JButton implements MouseListener{
  private String name;
  private Image img;

  public Bouton(String str){
    super(str);
    this.name = str;
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  this.addMouseListener(this);
  }

  public void paintComponent(Graphics g){
    Graphics2D g2d = (Graphics2D)g;
    GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
    g2d.setPaint(gp);
    g2d.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
    g2d.setColor(Color.black);
    g2d.drawString(this.name, this.getWidth() / 2 - (this.getWidth() / 2 /4), (this.getHeight() / 2) + 5);
  }

  public void mouseClicked(MouseEvent event) {
    //Inutile d'utiliser cette méthode ici                      
  }

  public void mouseEntered(MouseEvent event) {
    //Nous changeons le fond de notre image pour le jaune lors du survol, avec le fichier fondBoutonHover.png
    try {
      img = ImageIO.read(new File("fondBoutonHover.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void mouseExited(MouseEvent event) {
  //Nous changeons le fond de notre image pour le vert lorsque nous quittons le bouton, avec le fichier fondBouton.png
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public void mousePressed(MouseEvent event) {
    //Nous changeons le fond de notre image pour le jaune lors du clic gauche, avec le fichier fondBoutonClic.png
    try {
      img = ImageIO.read(new File("fondBoutonClic.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 
  public void mouseReleased(MouseEvent event) {
    //Nous changeons le fond de notre image pour le orange lorsque nous relâchons le clic, avec le fichier fondBoutonHover.png
    try {
      img = ImageIO.read(new File("fondBoutonHover.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }               
  }       
}

Et voilà le travail ! Si vous avez enregistré mes images, elles ne possèdent probablement pas le même nom que dans mon code : vous devez alors modifier le code en fonction de celui que vous leur avez attribué ! D'accord, ça va de soi… mais on ne sait jamais.

Vous possédez dorénavant un bouton personnalisé qui réagit au passage de votre souris. Je sais qu'il y aura des « p'tits malins » qui cliqueront sur le bouton et relâcheront le clic en dehors du bouton : dans ce cas, le fond du bouton deviendra orange, puisque c'est ce qui doit être effectué vu la méthodemouseReleased(). Afin de pallier ce problème, nous allons vérifier que lorsque le clic est relâché, la souris se trouve toujours sur le bouton.

Nous avons implémenté l'interfaceMouseListener ; il reste cependant un objet que nous n'avons pas encore utilisé. Vous ne le voyez pas ? C'est le paramètre présent dans toutes les méthodes de cette interface : oui, c'estMouseEvent !

Cet objet nous permet d'obtenir beaucoup d'informations sur les événements. Nous ne détaillerons pas tout ici, mais nous verrons certains côtés pratiques de ce type d'objet tout au long de cette partie. Dans notre cas, nous pouvons récupérer les coordonnées x et y du curseur de la souris par rapport auBoutongrâce aux méthodesgetX()etgetY(). Cela signifie que si nous relâchons le clic en dehors de la zone où se trouve notre objet, la valeur retournée par la méthodegetY()sera négative.

Voici le correctif d‌e la méthodemouseReleased()de notre classeBouton :

public void mouseReleased(MouseEvent event) {
  //Nous changeons le fond de notre image pour le orange lorsque nous relâchons le clic avec le fichier fondBoutonHover.png si la souris est toujours sur le bouton
  if((event.getY() > 0 && event.getY() < this.getHeight()) && (event.getX() > 0 && event.getX() < this.getWidth())){
    try {
      img = ImageIO.read(new File("fondBoutonHover.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  //Si on se trouve à l'extérieur, on dessine le fond par défaut
  else{
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }               
}

Nous possédons à présent un bouton réactif, mais qui n'effectue rien pour le moment. Je vous propose de combler cette lacune.

Interagir avec son bouton

Déclencher une action : l'interfaceActionListener

Afin de gérer les différentes actions à effectuer selon le bouton sur lequel on clique, nous allons utiliser l'interfaceActionListener.

Nous n'allons pas implémenter cette interface dans notre classeBoutonmais dans notre classeFenetre, le but étant de faire en sorte que lorsque l'on clique sur le bouton, il se passe quelque chose dans notre application : changer un état, une variable, effectuer une incrémentation… Enfin, n'importe quelle action !

Comme je vous l'ai expliqué, lorsque nous appliquons unaddMouseListener(), nous informons l'objet observé qu'un autre objet doit être tenu au courant de l'événement. Ici, nous voulons que ce soit notre application (notreFenetre) qui écoute notreBouton, le but étant de pouvoir lancer ou arrêter l'animation dans lePanneau.

Avant d'en arriver là, nous allons faire plus simple : nous nous pencherons dans un premier temps sur l'implémentation de l'interfaceActionListener. Afin de vous montrer toute la puissance de cette interface, nous utiliserons un nouvel objet issu du packagejavax.swing : leJLabel. Cet objet se comporte comme un libellé : il est spécialisé dans l'affichage de texte ou d'image. Il est donc idéal pour notre premier exemple !

On l'instancie et l'initialise plus ou moins de la même manière que leJButton :

JLabel label1 = new JLabel();
label1.setText("Mon premier JLabel");
//Ou encore
JLabel label2 = new JLabel("Mon deuxième JLabel");

Créez une variable d'instance de typeJLabel(appelez-lalabel) et initialisez-la avec le texte qui vous plaît ; ajoutez-la ensuite à votre content pane en positionBorderLayout.NORTH.

Le résultat se trouve en figure suivante.

Utilisation d'un JLabel
Utilisation d'un JLabel

Voici le code correspondant :

public class Fenetre extends JFrame {
  private Panneau pan = new Panneau();
  private Bouton bouton = new Bouton("mon bouton");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);

    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
    container.add(bouton, BorderLayout.SOUTH);
    container.add(label, BorderLayout.NORTH);

    this.setContentPane(container);
    this.setVisible(true);
    go();
  }
  //Le reste ne change pas
}

Vous pouvez voir que le texte de cet objet est aligné par défaut en haut à gauche. Il est possible de modifier quelques paramètres tels que :

  • l'alignement du texte ;

  • la police à utiliser ;

  • la couleur du texte ;

  • d'autres paramètres.

Voici un code mettant tout cela en pratique :

public Fenetre(){
  this.setTitle("Animation");
  this.setSize(300, 300);
  this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  this.setLocationRelativeTo(null);
 
  container.setBackground(Color.white);
  container.setLayout(new BorderLayout());
  container.add(pan, BorderLayout.CENTER);
  container.add(bouton, BorderLayout.SOUTH);
        
  //Définition d'une police d'écriture
  Font police = new Font("Tahoma", Font.BOLD, 16);
  //On l'applique au JLabel
  label.setFont(police);
  //Changement de la couleur du texte
  label.setForeground(Color.blue);
  //On modifie l'alignement du texte grâce aux attributs statiques
  //de la classe JLabel
  label.setHorizontalAlignment(JLabel.CENTER);
        
  container.add(label, BorderLayout.NORTH);
  this.setContentPane(container);
  this.setVisible(true);
  go();
}

La figure suivante donne un aperçu de ce code.

Utilisation plus fine d'un JLabel
Utilisation plus fine d'un JLabel

Maintenant que notre libellé se présente exactement sous la forme que nous voulons, nous pouvons implémenter l'interfaceActionListener. Vous remarquerez que cette interface ne contient qu'une seule méthode !

//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame implements ActionListener{
  private Panneau pan = new Panneau();
  private Bouton bouton = new Bouton("mon bouton");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");

  public Fenetre(){
    //Ce morceau de code ne change pas
  }

  //Méthode qui sera appelée lors d'un clic sur le bouton
  public void actionPerformed(ActionEvent arg0) {      

  } 
}

Nous allons maintenant informer notre objetBoutonque notre objetFenetrel'écoute. Vous l'avez deviné : ajoutons notreFenetreà la liste des objets qui écoutent notreBoutongrâce à la méthodeaddActionListener(ActionListener obj)présente dans la classeJButton, donc utilisable avec la variablebouton. Ajoutons cette instruction dans le constructeur en passantthisen paramètre (puisque c'est notreFenetrequi écoute leBouton).

Une fois l'opération effectuée, nous pouvons modifier le texte duJLabelavec la méthodeactionPerformed(). Nous allons compter le nombre de fois que l'on a cliqué sur le bouton : ajoutons une variable d'instance de typeintdans notre class et appelons-lacompteur, puis dans la méthodeactionPerformed(), incrémentons ce compteur et affichons son contenu dans notre libellé.

Voici le code de notre objet mis à jour :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame implements ActionListener{
  private Panneau pan = new Panneau();
  private Bouton bouton = new Bouton("mon bouton");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");
  //Compteur de clics
  private int compteur = 0;
  
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
 
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
        
    //Nous ajoutons notre fenêtre à la liste des auditeurs de notre bouton
    bouton.addActionListener(this);
        
    container.add(bouton, BorderLayout.SOUTH);
          
    Font police = new Font("Tahoma", Font.BOLD, 16);  
    label.setFont(police);  
    label.setForeground(Color.blue);  
    label.setHorizontalAlignment(JLabel.CENTER);
    container.add(label, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);
    go();
  }
      
  private void go(){
    //Cette méthode ne change pas
  }
 
   public void actionPerformed(ActionEvent arg0) {
    //Lorsque l'on clique sur le bouton, on met à jour le JLabel
    this.compteur++;
    label.setText("Vous avez cliqué " + this.compteur + " fois");
  }      
}

Voyez le résultat à la figure suivante.

Interaction avec le bouton
Interaction avec le bouton

Et nous ne faisons que commencer… Eh oui, nous allons maintenant ajouter un deuxième bouton à notreFenetre, à côté du premier (vous êtes libres d'utiliser la classe personnalisée ou un simpleJButton). Pour ma part, j'utiliserai des boutons normaux ; en effet, dans notre classe personnalisée, la façon dont le libellé est écrit dans notre bouton n'est pas assez souple et l'affichage peut donc être décevant (dans certains cas, le libellé peut ne pas être centré)…

Bref, nous possédons à présent deux boutons écoutés par notre objetFenetre.

Voici notre nouveau code :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Fenetre extends JFrame implements ActionListener{
  private Panneau pan = new Panneau();
  private JButton bouton = new JButton("bouton 1");
  private JButton bouton2 = new JButton("bouton 2");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");
  private int compteur = 0;

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);

    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);

    bouton.addActionListener(this);
    bouton2.addActionListener(this);

    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);

    Font police = new Font("Tahoma", Font.BOLD, 16);
    label.setFont(police);
    label.setForeground(Color.blue);
    label.setHorizontalAlignment(JLabel.CENTER);
    container.add(label, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);
    go();
  }

  //…
}

La figure suivante illustre le résultat que nous obtenons.

Un deuxième bouton dans la fenêtre
Un deuxième bouton dans la fenêtre

À présent, le problème est le suivant : comment effectuer deux actions différentes dans la méthodeactionPerformed() ?

En effet, si nous laissons la méthodeactionPerformed()telle quelle, les deux boutons exécutent la même action lorsqu'on les clique. Essayez, vous verrez le résultat.

Il existe un moyen de connaître l'élément ayant déclenché l'événement : il faut se servir de l'objet passé en paramètre dans la méthodeactionPerformed(). Nous pouvons exploiter la méthodegetSource()de cet objet pour connaître le nom de l'instance qui a généré l'événement. Testez la méthodeactionPerformed()suivante et voyez si le résultat correspond à la figure suivante.

public void actionPerformed(ActionEvent arg0) {
  if(arg0.getSource() == bouton)
    label.setText("Vous avez cliqué sur le bouton 1");

  if(arg0.getSource() == bouton2)
    label.setText("Vous avez cliqué sur le bouton 2");
}
Détection de la source de l'événement
Détection de la source de l'événement

Notre code fonctionne à merveille ! Cependant, cette approche n'est pas très orientée objet : si notre IHM contient une multitude de boutons, la méthodeactionPerformed()sera très chargée. Nous pourrions créer deux objets à part, chacun écoutant un bouton, dont le rôle serait de réagir de façon appropriée pour chaque bouton ; mais si nous avions besoin de modifier des données spécifiques à la classe contenant nos boutons, il faudrait ruser afin de parvenir à faire communiquer nos objets… Pas terrible non plus.

Parler avec sa classe intérieure

En Java, on peut créer ce que l'on appelle des classes internes. Cela consiste à déclarer une classe à l'intérieur d'une autre classe. Je sais, ça peut paraître tordu, mais vous allez bientôt constater que c'est très pratique.

En effet, les classes internes possèdent tous les avantages des classes normales, de l'héritage d'une superclasse à l'implémentation d'une interface. Elles bénéficient donc du polymorphisme et de la covariance des variables. En outre, elles ont l'avantage d'avoir accès aux attributs de la classe dans laquelle elles sont déclarées !

Dans le cas qui nous intéresse, cela permet de créer une implémentation de l'interfaceActionListenerdétachée de notre classeFenetre, mais pouvant utiliser ses attributs. La déclaration d'une telle classe se fait exactement de la même manière que pour une classe normale, si ce n'est qu'elle se trouve déjà dans une autre classe. Nous procédons donc comme ceci :

public class MaClasseExterne{

  public MaClasseExterne(){
    //…
  }

  class MaClassInterne{
    public MaClassInterne(){
      //…
    }
  }
}

Grâce à cela, nous pourrons concevoir une classe spécialisée dans l'écoute des composants et qui effectuera un travail bien déterminé. Dans notre exemple, nous créerons deux classes internes implémentant chacune l'interfaceActionListeneret redéfinissant la méthodeactionPerformed() :

  • BoutonListenerécoutera le premier bouton ;

  • Bouton2Listenerécoutera le second.

Une fois ces opérations effectuées, il ne nous reste plus qu'à indiquer à chaque bouton « qui l'écoute » grâce à la méthodeaddActionListener().

Voyez ci-dessous la classeFenetremise à jour.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame{
 
  private Panneau pan = new Panneau();
  private JButton bouton = new JButton("bouton 1");
  private JButton bouton2 = new JButton("bouton 2");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");
  private int compteur = 0;
  
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
 
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
        
    //Ce sont maintenant nos classes internes qui écoutent nos boutons 
    bouton.addActionListener(new BoutonListener());
    bouton2.addActionListener(new Bouton2Listener());
        
    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);
    Font police = new Font("Tahoma", Font.BOLD, 16);
    label.setFont(police);
    label.setForeground(Color.blue);
    label.setHorizontalAlignment(JLabel.CENTER);
    container.add(label, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);
    go();
  }
      
  private void go(){
    //Cette méthode ne change pas
  }
      
  //Classe écoutant notre premier bouton
  class BoutonListener implements ActionListener{
    //Redéfinition de la méthode actionPerformed()
    public void actionPerformed(ActionEvent arg0) {
      label.setText("Vous avez cliqué sur le bouton 1");        
    }
  }
      
  //Classe écoutant notre second bouton
  class Bouton2Listener implements ActionListener{
    //Redéfinition de la méthode actionPerformed()
    public void actionPerformed(ActionEvent e) {
      label.setText("Vous avez cliqué sur le bouton 2");    
    }
  }      
}

Le résultat, visible à la figure suivante, est parfait.

Utilisation de deux actions sur deux boutons
Utilisation de deux actions sur deux boutons

Dorénavant, nous n'avons plus à nous soucier du bouton qui a déclenché l'événement, car nous disposons de deux classes écoutant chacune un bouton. Nous pouvons souffler un peu : une grosse épine vient de nous être retirée du pied.

Eh oui, faites le test : créez une troisième classe interne et attribuez-lui le nom que vous voulez (personnellement, je l'ai appeléeBouton3Listener). Implémentez-y l'interfaceActionListeneret contentez-vous d'effectuer un simpleSystem.out.println()dans la méthodeactionPerformed(). N'oubliez pas de l'ajouter à la liste des classes qui écoutent votre bouton (n'importe lequel des deux ; j'ai pour ma part choisi le premier).

Je vous écris uniquement le code ajouté :

//Les imports…

public class Fenetre extends JFrame{
  //Les variables d'instance…
  
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
 
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
        
    //Première classe écoutant mon premier bouton 
    bouton.addActionListener(new BoutonListener());
    //Deuxième classe écoutant mon premier bouton
    bouton.addActionListener(new Bouton3Listener());
        
    bouton2.addActionListener(new Bouton2Listener());
        
    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);
        
    Font police = new Font("Tahoma", Font.BOLD, 16);
    label.setFont(police);
    label.setForeground(Color.blue);
    label.setHorizontalAlignment(JLabel.CENTER);
    container.add(label, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);
    go();
  }
 
  //…
 
  class Bouton3Listener implements ActionListener{
    //Redéfinition de la méthode actionPerformed()
    public void actionPerformed(ActionEvent e) {
      System.out.println("Ma classe interne numéro 3 écoute bien !");          
    }
  }
}

Le résultat se trouve sur la figure suivante.

Deux écouteurs sur un bouton
Deux écouteurs sur un bouton

Les classes internes sont vraiment des classes à part entière. Elles peuvent également hériter d'une superclasse. De ce fait, c'est presque comme si nous nous trouvions dans le cas d'un héritage multiple (ce n'en est pas un, même si cela y ressemble). Ce code est donc valide :

public class MaClasseExterne extends JFrame{

  public MaClasseExterne(){
    //...
  }

  class MaClassInterne extends JPanel{
    public MaClassInterne(){
      //…
    }
  }

  class MaClassInterne2 extends JButton{
    public MaClassInterne(){
      //…
    }
  }
}

Vous voyez bien que ce genre de classes peut s'avérer très utile.

Bon, nous avons réglé le problème d'implémentation : nous possédons deux boutons qui sont écoutés. Il ne nous reste plus qu'à lancer et arrêter notre animation à l'aide de ces boutons. Mais auparavant, nous allons étudier une autre manière d'implémenter des écouteurs et, par extension, des classes devant redéfinir les méthodes d'une classe abstraite ou d'une interface.

Les classes anonymes

Il n'y a rien de compliqué dans cette façon de procéder, mais je me rappelle avoir été déconcerté lorsque je l'ai rencontrée pour la première fois.

Les classes anonymes sont le plus souvent utilisées pour la gestion d'événements ponctuels, lorsque créer une classe pour un seul traitement est trop lourd. Rappelez-vous ce que j'ai utilisé pour définir le comportement de mes boutons lorsque je vous ai présenté l'objetCardLayout: c'étaient des classes anonymes. Pour rappel, voici ce que je vous avais amenés à coder :

JButton bouton = new JButton("Contenu suivant");
//Définition de l'action sur le bouton
bouton.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent event){
    //Action !
  }
});

L'une des particularités de cette méthode, c'est que l'écouteur n'écoutera que ce composant. Vous pouvez vérifier qu'il n'y se trouve aucune déclaration de classe et que nous instancions une interface par l'instructionnew ActionListener(). Nous devons seulement redéfinir la méthode, que vous connaissez bien maintenant, dans un bloc d'instructions ; d'où les accolades après l'instanciation, comme le montre la figure suivante.

Découpage d'une classe anonyme
Découpage d'une classe anonyme

Pourquoi appelle-t-on cela une classe « anonyme » ?

C'est simple : procéder de cette manière revient à créer une classe fille sans être obligé de créer cette classe de façon explicite. L'héritage se produit automatiquement. En fait, le code ci-dessus revient à effectuer ceci :

class Fenetre extends JFrame{
  //…
  bouton.addActionListener(new ActionListenerBis());
  //…

  public class ActionListenerBis implements ActionListener{
    public void actionPerformed(ActionEvent event){
      //Action !
    }
  }
}

Seulement, la classe créée n'a pas de nom, l'héritage s'effectue de façon implicite ! Nous bénéficions donc de tous les avantages de la classe mère en ne redéfinissant que la méthode qui nous intéresse.

Sachez aussi que les classes anonymes peuvent être utilisées pour implémenter des classes abstraites. Je vous conseille d'effectuer de nouveaux tests en utilisant notre exemple du pattern strategy ; mais cette fois, plutôt que de créer des classes, créez des classes anonymes.

Les classes anonymes sont soumises aux mêmes règles que les classes « normales » :

  • utilisation des méthodes non redéfinies de la classe mère ;

  • obligation de redéfinir toutes les méthodes d'une interface ;

  • obligation de redéfinir les méthodes abstraites d'une classe abstraite.

Cependant, ces classes possèdent des restrictions à cause de leur rôle et de leur raison d'être :

  • elles ne peuvent pas être déclaréesabstract ;

  • elles ne peuvent pas non plus être déclaréesstatic ;

  • elles ne peuvent pas définir de constructeur ;

  • elles sont automatiquement déclaréesfinal : on ne peut dériver de cette classe, l'héritage est donc impossible !

Contrôler son animation : lancement et arrêt

Pour parvenir à gérer le lancement et l'arrêt de notre animation, nous allons devoir modifier un peu le code de notre classeFenetre. Il va falloir changer le libellé des boutons de notre IHM : le premier afficheraGoet le deuxièmeStop. Pour éviter d'interrompre l'animation alors qu'elle n'est pas lancée et de l'animer quand elle l'est déjà, nous allons tantôt activer et désactiver les boutons. Je m'explique :

  • au lancement, le boutonGone sera pas cliquable alors que le boutonStopoui ;

  • si l'animation est interrompue, le boutonStopne sera plus cliquable, mais le boutonGole sera.

Ne vous inquiétez pas, c'est très simple à réaliser. Il existe une méthode gérant ces changements d'état :

JButton bouton = new JButton("bouton");
bouton.setEnabled(false); //Le bouton n'est plus cliquable
bouton.setEnabled(true);  //Le bouton est de nouveau cliquable

Ces objets permettent de réaliser pas mal de choses ; soyez curieux et testez-en les méthodes. Allez donc faire un tour sur le site d'Oracle : fouillez, fouinez…

L'une de ces méthodes, qui s'avère souvent utile et est utilisable avec tous ces objets (ainsi qu'avec les objets que nous verrons par la suite), est la méthode de gestion de dimension. Il ne s'agit pas de la méthodesetSize(), mais de la méthodesetPreferredSize(). Elle prend en paramètre un objetDimension, qui, lui, prend deux entiers comme arguments.

Voici un exemple :

bouton.setPreferredSize(new Dimension(150, 120));

En l'utilisant dans notre application, nous obtenons la figure suivante.

Gestion de la taille de nos boutons
Gestion de la taille de nos boutons

Afin de bien gérer notre animation, nous devons améliorer notre méthodego(). Sortons donc de cette méthode les deux entiers dont nous nous servions afin de recalculer les coordonnées de notre rond. La boucle infinie doit dorénavant pouvoir être interrompue ! Pour réussir cela, nous allons déclarer un booléen qui changera d'état selon le bouton sur lequel on cliquera ; nous l'utiliserons comme paramètre de notre boucle.

Voyez ci-dessous le code de notre classeFenetre.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Fenetre extends JFrame{

  private Panneau pan = new Panneau();
  private JButton bouton = new JButton("Go");
  private JButton bouton2 = new JButton("Stop");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Le JLabel");
  private int compteur = 0;
  private boolean animated = true;
  private boolean backX, backY;
  private int x, y;

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);

    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
    bouton.addActionListener(new BoutonListener()); 
    bouton.setEnabled(false);
    bouton2.addActionListener(new Bouton2Listener());

    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);
    Font police = new Font("Tahoma", Font.BOLD, 16);
    label.setFont(police);
    label.setForeground(Color.blue);
    label.setHorizontalAlignment(JLabel.CENTER);
    container.add(label, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);
    go();
  }

  private void go(){
    //Les coordonnées de départ de notre rond
    x = pan.getPosX();
    y = pan.getPosY();
    //Dans cet exemple, j'utilise une boucle while
    //Vous verrez qu'elle fonctionne très bien
    while(this.animated){
      if(x < 1)backX = false;
      if(x > pan.getWidth()-50)backX = true;          
      if(y < 1)backY = false;
      if(y > pan.getHeight()-50)backY = true;
      if(!backX)pan.setPosX(++x);
      else pan.setPosX(--x);
      if(!backY) pan.setPosY(++y);
      else pan.setPosY(--y);
      pan.repaint();

      try {
        Thread.sleep(3);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }     
  }

  class BoutonListener implements ActionListener{
    public void actionPerformed(ActionEvent arg0) {
      animated = true;
      bouton.setEnabled(false);
      bouton2.setEnabled(true);
      go();
    }
  }

  class Bouton2Listener implements ActionListener{
     public void actionPerformed(ActionEvent e) {
      animated = false;     
      bouton.setEnabled(true);
      bouton2.setEnabled(false);
    }
  }     
}

À l'exécution, vous remarquez que :

  • le boutonGon'est pas cliquable et l'autre l'est ;

  • l'animation se lance ;

  • l'animation s'arrête lorsque l'on clique sur le boutonStop ;

  • le boutonGodevient alors cliquable ;

  • lorsque vous cliquez dessus, l'animation ne se relance pas !

Comment est-ce possible ?

Comme je l'ai expliqué dans le chapitre traitant des conteneurs, un thread est lancé au démarrage de notre application : c'est le processus principal du programme. Au démarrage, l'animation est donc lancée dans le même thread que notre objetFenetre. Lorsque nous lui demandons de s'arrêter, aucun problème : les ressources mémoire sont libérées, on sort de la boucle infinie et l'application continue à fonctionner.

Mais lorsque nous redemandons à l'animation de se lancer, l'instruction se trouvant dans la méthodeactionPerformed()appelle la méthodego()et, étant donné que nous nous trouvons à l'intérieur d'une boucle infinie, nous restons dans la méthodego()et ne sortons plus de la méthodeactionPerformed().

Explication de ce phénomène

Java gère les appels aux méthodes grâce à ce que l'on appelle vulgairement la pile.

Pour expliquer cela, prenons un exemple tout bête ; regardez cet objet :

public class TestPile {
  public TestPile(){
    System.out.println("Début constructeur");
    methode1();
    System.out.println("Fin constructeur");
  }

  public void methode1(){
      System.out.println("Début méthode 1");
      methode2();
      System.out.println("Fin méthode 1");
   }
        
   public void methode2(){
    System.out.println("Début méthode 2");
    methode3();
    System.out.println("Fin méthode 2");
  }

  public void methode3(){
    System.out.println("Début méthode 3");
    System.out.println("Fin méthode 3");
  }
}

Si vous instanciez cet objet, vous obtenez dans la console la figure suivante.

Exemple de pile d'invocations
Exemple de pile d'invocations

Je suppose que vous avez remarqué avec stupéfaction que l'ordre des instructions est un peu bizarre. Voici ce qu'il se passe :

  • à l'instanciation, notre objet appelle la méthode 1 ;

  • cette dernière invoque la méthode 2 ;

  • celle-ci utilise la méthode 3 : une fois qu'elle a terminé, la JVM retourne dans la méthode 2 ;

  • lorsqu'elle a fini de s'exécuter, on remonte à la fin de la méthode 1, jusqu'à la dernière instruction appelante : le constructeur.

La figure suivante présente un schéma résumant la situation.

Empilage et dépilage de méthodes
Empilage et dépilage de méthodes

Dans notre programme, imaginez que la méthodeactionPerformed()soit représentée par la méthode 2, et que notre méthodego()soit représentée par la méthode 3. Lorsque nous entrons dans la méthode 3, nous entrons dans une boucle infinie… Conséquence directe : nous ne ressortons jamais de cette méthode et la JVM ne dépile plus !

Afin de pallier ce problème, nous allons utiliser un nouveau thread. Grâce à cela, la méthodego()se trouvera dans une pile à part.

Attends : on arrive pourtant à arrêter l'animation alors qu'elle se trouve dans une boucle infinie. Pourquoi ?

Tout simplement parce que nous ne demandons d'effectuer qu'une simple initialisation de variable dans la gestion de notre événement ! Si vous créez une deuxième méthode comprenant une boucle infinie et que vous l'invoquez lors du clic sur le boutonStop, vous aurez exactement le même problème.

Je ne vais pas m'éterniser là-dessus, nous verrons cela dans un prochain chapitre. À présent, je pense qu'il est de bon ton de vous parler du mécanisme d'écoute d'événements, le fameux pattern observer.

Être à l'écoute de ses objets : le design pattern Observer

Le design patternObserverest utilisé pour gérer les événements de vos IHM. C'est une technique de programmation. La connaître n'est pas absolument indispensable, mais cela vous aide à mieux comprendre le fonctionnement deSwingetAWT. C'est par ce biais que vos composants effectueront quelque chose lorsque vous les cliquerez ou les survolerez.

Je vous propose de découvrir son fonctionnement à l'aide d'une situation problématique.

Posons le problème

Sachant que vous êtes des développeurs Java chevronnés, un de vos amis proches vous demande si vous êtes en mesure de l'aider à réaliser une horloge digitale en Java. Il a en outre la gentillesse de vous fournir les classes à utiliser pour la création de son horloge. Votre ami a l'air de s'y connaître, car ce qu'il vous a fourni est bien structuré.

Packagecom.sdz.vue, classeFenetre.java
package com.sdz.vue;

import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.JFrame;
import javax.swing.JLabel;

import com.sdz.model.Horloge;

public class Fenetre extends JFrame{
  private JLabel label = new JLabel();
  private Horloge horloge;
	
  public Fenetre(){
    //On initialise la JFrame
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    this.setResizable(false);
    this.setSize(200, 80);
    //On initialise l'horloge
    this.horloge = new Horloge();
    //On initialise le JLabel
    Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
    this.label.setFont(police);
    this.label.setHorizontalAlignment(JLabel.CENTER);
    //On ajoute le JLabel à la JFrame
    this.getContentPane().add(this.label, BorderLayout.CENTER);		
  }

  //Méthode main() lançant le programme
  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }	
}
Packagecom.sdz.model, classeHorloge.java
package com.sdz.model;

import java.util.Calendar;

public class Horloge{
  //Objet calendrier pour récupérer l'heure courante
  private Calendar cal;
  private String hour = "";
	
  public void run() {
    while(true){
    //On récupère l'instance d'un calendrier à chaque tour 
    //Elle va nous permettre de récupérer l'heure actuelle
    this.cal = Calendar.getInstance();
    this.hour =  //Les heures
      this.cal.get(Calendar.HOUR_OF_DAY) + " : " 
      + 
      (    //Les minutes
        this.cal.get(Calendar.MINUTE) < 10
        ? "0" + this.cal.get(Calendar.MINUTE)
        : this.cal.get(Calendar.MINUTE)
      )
      + " : " 
      +
      (    //Les secondes
        (this.cal.get(Calendar.SECOND)< 10) 
        ? "0"+this.cal.get(Calendar.SECOND) 
        : this.cal.get(Calendar.SECOND)
      );
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

Le problème auquel votre ami est confronté est simple : il est impossible de faire communiquer l'horloge avec la fenêtre.

Je ne vois pas où est le problème : il n'a qu'à passer son instance deJLabeldans son objetHorloge, et le tour est joué !

En réalité, votre ami, dans son infinie sagesse, souhaite - je le cite - que l'horloge ne dépende pas de son interface graphique, juste au cas où il devrait passer d'une IHMswingà une IHMawt.

Il est vrai que si l'on passe l'objet d'affichage dans l'horloge, dans le cas où l'on change le type de l'IHM, toutes les classes doivent être modifiées ; ce n'est pas génial. En fait, lorsque vous procédez de la sorte, on dit que vous couplez des objets : vous rendez un ou plusieurs objets dépendants d'un ou de plusieurs autres objets (entendez par là que vous ne pourrez plus utiliser les objets couplés indépendamment des objets auxquels ils sont attachés).

Le couplage entre objets est l'un des problèmes principaux relatifs à la réutilisation des objets. Dans notre cas, si vous utilisez l'objetHorlogedans une autre application, vous serez confrontés à plusieurs problèmes étant donné que cet objet ne s'affiche que dans unJLabel.

C'est là que le pattern observer entre en jeu : il fait communiquer des objets entre eux sans qu'ils se connaissent réellement ! Vous devez être curieux de voir comment il fonctionne, je vous propose donc de l'étudier sans plus tarder.

Des objets qui parlent et qui écoutent : le pattern observer

Faisons le point sur ce que vous savez de ce pattern pour le moment :

  • il fait communiquer des objets entre eux ;

  • c'est un bon moyen d'éviter le couplage d'objets.

Ce sont deux points cruciaux, mais un autre élément, que vous ne connaissez pas encore, va vous plaire : tout se fait automatiquement !

Comment les choses vont-elles alors se passer ? Réfléchissons à ce que nous voulons que notre horloge digitale effectue : elle doit pouvoir avertir l'objet servant à afficher l'heure lorsqu'il doit rafraîchir son affichage. Puisque les horloges du monde entier se mettent à jour toutes les secondes, il n'y a aucune raison pour que la nôtre ne fasse pas de même.

Ce qui est merveilleux avec ce pattern, c'est que notre horloge ne se contentera pas d'avertir un seul objet que sa valeur a changé : elle pourra en effet mettre plusieurs observateurs au courant !

En fait, pour faire une analogie, interprétez la relation entre les objets implémentant le pattern observer comme un éditeur de journal et ses clients (voir figure suivante).

Livreur de journaux
Livreur de journaux

Grâce à ce schéma, vous pouvez sentir que notre objet défini comme observable pourra être surveillé par plusieurs objets : il s'agit d'une relation dite de un à plusieurs vers l'objetObservateur. Avant de vous expliquer plus en détail le fonctionnement de ce pattern, jetez un œil au diagramme de classes de notre application en figure suivante.

Diagramme de classes du pattern observer
Diagramme de classes du pattern observer

Ce diagramme indique que ce ne sont pas les instances d'Horlogeou deJLabelque nous allons utiliser, mais des implémentations d'interfaces.

En effet, vous savez que les classes implémentant une interface peuvent être définies par le type de l'interface. Dans notre cas, la classeFenetreimplémentera l'interfaceObservateur: nous pourrons la voir comme une classe du typeObservateur. Vous avez sans doute remarqué que la deuxième interface - celle dédiée à l'objetHorloge - possède trois méthodes :

  • une permettant d'ajouter des observateurs (nous allons donc gérer une collection d'observateurs) ;

  • une permettant de retirer les observateurs ;

  • enfin, une mettant à jour les observateurs.

Grâce à cela, nos objets ne sont plus liés par leurs types, mais par leurs interfaces ! L'interface qui apportera les méthodes de mise à jour, d'ajout d'observateurs, etc. travaillera donc avec des objets de typeObservateur.

Ainsi, le couplage ne s'effectue plus directement, il s'opère par le biais de ces interfaces. Ici, il faut que nos deux interfaces soient couplées pour que le système fonctionne. De même que, lorsque je vous ai présenté le pattern decorator, nos classes étaient très fortement couplées puisqu'elles devaient travailler ensemble : nous devions alors faire en sorte de ne pas les séparer.

Voici comment fonctionnera l'application :

  • nous instancierons la classeHorlogedans notre classeFenetre ;

  • cette dernière implémentera l'interfaceObservateur ;

  • notre objetHorloge, implémentant l'interfaceObservable, préviendra les objets spécifiés de ses changements ;

  • nous informerons l'horloge que notre fenêtre l'observe ;

  • à partir de là, notre objetHorlogefera le reste : à chaque changement, nous appellerons la méthode mettant tous les observateurs à jour.

Le code source de ces interfaces se trouve ci-dessous (notez que j'ai créé un packagecom.sdz.observer).

Observateur.java
package com.sdz.observer;

public interface Observateur {
  public void update(String hour);
}
Observer.java
package com.sdz.observer;

public interface Observable {
  public void addObservateur(Observateur obs);
  public void updateObservateur();
  public void delObservateur();
}

Voici maintenant le code de nos deux classes, travaillant ensemble mais n'étant que faiblement couplées.

Horloge.java
package com.sdz.model;

import java.util.ArrayList;
import java.util.Calendar;

import com.sdz.observer.Observable;
import com.sdz.observer.Observateur;

public class Horloge implements Observable{
  //On récupère l'instance d'un calendrier 
  //Elle va nous permettre de récupérer l'heure actuelle
  private Calendar cal;
  private String hour = "";
  //Notre collection d'observateurs
  private ArrayList<Observateur> listObservateur = new ArrayList<Observateur>();
	
  public void run() {
    while(true){
      this.cal = Calendar.getInstance();
      this.hour =  //Les heures
        this.cal.get(Calendar.HOUR_OF_DAY) + " : " 
        + 
        (      //Les minutes
          this.cal.get(Calendar.MINUTE) < 10
          ? "0" + this.cal.get(Calendar.MINUTE)
          : this.cal.get(Calendar.MINUTE)
        )
        + " : " 
        +
        (      //Les secondes
          (this.cal.get(Calendar.SECOND)< 10) 
          ? "0"+this.cal.get(Calendar.SECOND) 
          : this.cal.get(Calendar.SECOND)
        );
      //On avertit les observateurs que l'heure a été mise à jour
      this.updateObservateur();
			
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  //Ajoute un observateur à la liste
  public void addObservateur(Observateur obs) {
    this.listObservateur.add(obs);
  }
  //Retire tous les observateurs de la liste
  public void delObservateur() {
    this.listObservateur = new ArrayList<Observateur>();
  }
  //Avertit les observateurs que l'objet observable a changé 
  //et invoque la méthode update() de chaque observateur
  public void updateObservateur() {
    for(Observateur obs : this.listObservateur )
      obs.update(this.hour);
  }
}
Fenetre.java
package com.sdz.vue;

import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.JFrame;
import javax.swing.JLabel;

import com.sdz.model.Horloge;
import com.sdz.observer.Observateur;

public class Fenetre extends JFrame {
  private JLabel label = new JLabel();
  private Horloge horloge;
	
  public Fenetre(){
    //On initialise la JFrame
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    this.setResizable(false);
    this.setSize(200, 80);
		
    //On initialise l'horloge
    this.horloge = new Horloge();
    //On place un écouteur sur l'horloge
    this.horloge.addObservateur(new Observateur(){
      public void update(String hour) {
        label.setText(hour);
      }
    });
		
    //On initialise le JLabel
    Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
    this.label.setFont(police);
    this.label.setHorizontalAlignment(JLabel.CENTER);
    //On ajoute le JLabel à la JFrame
    this.getContentPane().add(this.label, BorderLayout.CENTER);		
    this.setVisible(true);
    this.horloge.run();
  }

  //Méthode main() lançant le programme
  public static void main(String[] args){
    Fenetre fen = new Fenetre();
  }
}

Exécutez ce code, vous verrez que tout fonctionne à merveille. Vous venez de permettre à deux objets de communiquer en n'utilisant presque aucun couplage : félicitations !

Vous pouvez voir que lorsque l'heure change, la méthodeupdateObservateur()est invoquée. Celle-ci parcourt la collection d'objetsObservateuret appelle sa méthodeupdate(String hour). La méthode étant redéfinie dans notre classeFenetreafin de mettreJLabelà jour, l'heure s'affiche !

J'ai mentionné que ce pattern est utilisé dans la gestion événementielle d'interfaces graphiques. Vous avez en outre remarqué que leurs syntaxes sont identiques. En revanche, je vous ai caché quelque chose : il existe des classes Java permettant d'utiliser le pattern observer sans avoir à coder les interfaces.

Le pattern observer : le retour

En réalité, il existe une classe abstraiteObservableet une interfaceObserverfournies dans les classes Java.

Celles-ci fonctionnent de manière quasiment identique à notre façon de procéder, seuls quelques détails diffèrent. Personnellement, je préfère de loin utiliser un pattern observer « fait maison ».

Pourquoi cela ? Tout simplement parce que l'objet que l'on souhaite observer doit hériter de la classeObservable. Par conséquent, il ne pourra plus hériter d'une autre classe étant donné que Java ne gère pas l'héritage multiple. La figure suivante présente la hiérarchie de classes du pattern observer présent dans Java.

Diagramme de classes du pattern observer de Java
Diagramme de classes du pattern observer de Java

Vous remarquez qu'il fonctionne presque de la même manière que celui que nous avons développé. Il y a toutefois une différence dans la méthodeupdate(Observable obs, Object obj) : sa signature a changé.
Cette méthode prend ainsi deux paramètres :

  • un objetObservable ;

  • unObjectreprésentant une donnée supplémentaire que vous souhaitez lui fournir.

Vous connaissez le fonctionnement de ce pattern, il vous est donc facile de comprendre le schéma. Je vous invite cependant à effectuer vos propres recherches sur son implémentation dans Java : vous verrez qu'il existe des subtilités (rien de méchant, cela dit).

Cadeau : un bouton personnalisé optimisé

Terminons par une version améliorée de notre bouton qui reprend ce que nous avons appris :

import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException; 
import javax.imageio.ImageIO;
import javax.swing.JButton;
 
public class Bouton extends JButton implements MouseListener{
  private String name;
  private Image img;

  public Bouton(String str){
    super(str);
    this.name = str;
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }
    this.addMouseListener(this);
  }

  public void paintComponent(Graphics g){
    Graphics2D g2d = (Graphics2D)g;
    GradientPaint gp = new GradientPaint(0, 0, Color.blue, 0, 20, Color.cyan, true);
    g2d.setPaint(gp);
    g2d.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
    g2d.setColor(Color.black);
   
    //Objet permettant de connaître les propriétés d'une police, dont la taille
    FontMetrics fm = g2d.getFontMetrics();
    //Hauteur de la police d'écriture
    int height = fm.getHeight();
    //Largeur totale de la chaîne passée en paramètre
    int width = fm.stringWidth(this.name);

    //On calcule alors la position du texte, et le tour est joué
    g2d.drawString(this.name, this.getWidth() / 2 - (width / 2), (this.getHeight() / 2) + (height / 4));     
  }

  public void mouseClicked(MouseEvent event) {
    //Inutile d'utiliser cette méthode ici       
  }

  public void mouseEntered(MouseEvent event) {    
    //Nous changeons le fond de notre image pour le jaune lors du survol, avec le fichier fondBoutonHover.png
    try {
      img = ImageIO.read(new File("fondBoutonHover.png"));   
    } catch (IOException e) {
      e.printStackTrace();
    }    
  }

  public void mouseExited(MouseEvent event) {
    //Nous changeons le fond de notre image pour le vert lorsque nous quittons le bouton, avec le fichier fondBouton.png
    try {
      img = ImageIO.read(new File("fondBouton.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }    
  }

  public void mousePressed(MouseEvent event) {
    //Nous changeons le fond de notre image pour le jaune lors du clic gauche, avec le fichier fondBoutonClic.png
    try {
      img = ImageIO.read(new File("fondBoutonClic.png"));
    } catch (IOException e) {
      e.printStackTrace();
    }    
  }

  public void mouseReleased(MouseEvent event) {
    //Nous changeons le fond de notre image pour l'orange lorsque nous relâchons le clic avec le fichier fondBoutonHover.png si la souris est toujours sur le bouton
    if((event.getY() > 0 && event.getY() < this.getHeight()) && (event.getX() > 0 && event.getX() < this.getWidth())){
      try {
        img = ImageIO.read(new File("fondBoutonHover.png"));
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    //Si on se trouve à l'extérieur, on dessine le fond par défaut
    else{
      try {
        img = ImageIO.read(new File("fondBouton.png")); 
      } catch (IOException e) {
        e.printStackTrace();
      }
    }   
  }   
}

Essayez, vous verrez que cette application fonctionne correctement.

  • Vous pouvez interagir avec un composant grâce à votre souris en implémentant l'interfaceMouseListener.

  • Lorsque vous implémentez une interface< … >Listener, vous indiquez à votre classe qu'elle doit se préparer à observer des événements du type de l'interface. Vous devez donc spécifier qui doit observer et ce que la classe doit observer grâce aux méthodes de typeadd< … >Listener(< … >Listener).

  • L'interface utilisée dans ce chapitre estActionListenerissue du packagejava.awt.

  • La méthode à implémenter de cette interface estactionPerformed().

  • Une classe interne est une classe se trouvant à l'intérieur d'une classe.

  • Une telle classe a accès à toutes les données et méthodes de sa classe externe.

  • La JVM traite les méthodes appelées en utilisant une pile qui définit leur ordre d'exécution.

  • Une méthode est empilée à son invocation, mais n'est dépilée que lorsque toutes ses instructions ont fini de s'exécuter.

  • Le pattern observer permet d'utiliser des objets faiblement couplés. Grâce à ce pattern, les objets restent indépendants.

Example of certificate of achievement
Example of certificate of achievement