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
- Interagir avec son bouton
- Être à l'écoute de ses objets : le design pattern Observer
- Cadeau : un bouton personnalisé optimisé
Une classe Bouton personnalisée
Créons une classe héritant de javax.swing.JButton
que nous appellerons Bouton
et redéfinissons sa méthode paintComponent()
. Vous devriez y arriver tout seuls. Cet exemple est représenté à la figure suivante :
Voici la classe Bouton
de cette application :
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 | 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.
Voyez le résultat en figure suivante.
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 notre Panneau
. Voici le code de cette classe Bouton
:
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 | 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'interface MouseListener
Avant de nous lancer dans l'implémentation, vous pouvez voir le résultat que nous allons obtenir sur les deux figures suivantes.
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'interface MouseListener
dans notre classe Bouton
. 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éthode addMouseListener(MouseListener obj)
qui prend un objet MouseListener
en paramètre (ici, elle prendra this
). Rappelez-vous que vous pouvez utiliser le type d'une interface comme supertype : ici, notre classe implémente l'interface MouseListener
, nous pouvons donc utiliser cet objet comme référence de cette interface.
Voici à présent notre classe Bouton
:
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 | 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'interface MouseListener
que 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.
Dans notre cas, la méthode repaint()
est appelée de façon implicite : lorsqu'un événement est déclenché, notre objet se redessine automatiquement ! Comme lorsque vous redimensionniez votre fenêtre dans les premiers chapitres.
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).
Je vous rappelle que dans le code qui suit, les images sont placées à la racine du projet.
Voici maintenant le code de notre classe Bouton
personnalisée :
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 73 74 75 76 | 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éthode mouseReleased()
. 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'interface MouseListener
; 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'est MouseEvent
!
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 au Bouton
grâce aux méthodes getX()
et getY()
. 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éthode getY()
sera négative.
Voici le correctif de la méthode mouseReleased()
de notre classe Bouton
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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() < bouton.getHeight()) && (event.getX() > 0 && event.getX() < bouton.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(); } } } |
Vous verrez dans les chapitres qui suivent qu'il existe plusieurs interfaces pour les différentes actions possibles sur une IHM. Sachez qu'il existe aussi une convention pour ces interfaces : leur nom commence par le type de l'action, suivi du mot Listener
. Nous avons étudié ici les actions de la souris, voyez le nom de l'interface : MouseListener
.
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'interface ActionListener
Afin de gérer les différentes actions à effectuer selon le bouton sur lequel on clique, nous allons utiliser l'interface ActionListener
.
Nous n'allons pas implémenter cette interface dans notre classe Bouton
mais dans notre classe Fenetre
, 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 un addMouseListener()
, 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 (notre Fenetre
) qui écoute notre Bouton
, le but étant de pouvoir lancer ou arrêter l'animation dans le Panneau
.
Avant d'en arriver là, nous allons faire plus simple : nous nous pencherons dans un premier temps sur l'implémentation de l'interface ActionListener
. Afin de vous montrer toute la puissance de cette interface, nous utiliserons un nouvel objet issu du package javax.swing
: le JLabel
. 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 le JButton
:
1 2 3 4 | 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 type JLabel
(appelez-la label
) et initialisez-la avec le texte qui vous plaît ; ajoutez-la ensuite à votre content pane en position BorderLayout.NORTH
.
Le résultat se trouve en figure suivante.
Voici le code correspondant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 :
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 | 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.
Maintenant que notre libellé se présente exactement sous la forme que nous voulons, nous pouvons implémenter l'interface ActionListener
. Vous remarquerez que cette interface ne contient qu'une seule méthode !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //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 objet Bouton
que notre objet Fenetre
l'écoute. Vous l'avez deviné : ajoutons notre Fenetre
à la liste des objets qui écoutent notre Bouton
grâce à la méthode addActionListener(ActionListener obj)
présente dans la classe JButton
, donc utilisable avec la variable bouton
. Ajoutons cette instruction dans le constructeur en passant this
en paramètre (puisque c'est notre Fenetre
qui écoute le Bouton
).
Une fois l'opération effectuée, nous pouvons modifier le texte du JLabel
avec la méthode actionPerformed()
. Nous allons compter le nombre de fois que l'on a cliqué sur le bouton : ajoutons une variable d'instance de type int
dans notre class et appelons-la compteur
, puis dans la méthode actionPerformed()
, incrémentons ce compteur et affichons son contenu dans notre libellé.
Voici le code de notre objet mis à jour :
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 | 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.
Et nous ne faisons que commencer… Eh oui, nous allons maintenant ajouter un deuxième bouton à notre Fenetre
, à côté du premier (vous êtes libres d'utiliser la classe personnalisée ou un simple JButton
). 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 objet Fenetre
.
Vous devez créer un deuxième JPanel
qui contiendra nos deux boutons, puis l'insérer dans le content pane en position BorderLayout.SOUTH
. Si vous tentez de positionner deux composants au même endroit grâce à un BorderLayout
,seul le dernier composant ajouté apparaîtra : en effet, le composant occupe toute la place disponible dans un BorderLayout
!
Voici notre nouveau code :
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 | 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.
À présent, le problème est le suivant : comment effectuer deux actions différentes dans la méthode actionPerformed()
?
En effet, si nous laissons la méthode actionPerformed()
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éthode actionPerformed()
. Nous pouvons exploiter la méthode getSource()
de cet objet pour connaître le nom de l'instance qui a généré l'événement. Testez la méthode actionPerformed()
suivante et voyez si le résultat correspond à la figure suivante.
1 2 3 4 5 6 7 | 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"); } |
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éthode actionPerformed()
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'interface ActionListener
détachée de notre classe Fenetre
, 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 :
1 2 3 4 5 6 7 8 9 10 11 12 | 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'interface ActionListener
et redéfinissant la méthode actionPerformed()
:
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éthode addActionListener()
.
Voyez ci-dessous la classe Fenetre
mise à jour.
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 | 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.
Vous pouvez constater que nos classes internes ont même accès aux attributs déclarés private
dans notre classe Fenetre
.
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.
Vous pouvez aussi faire écouter votre bouton par plusieurs classes. Il vous suffit d'ajouter ces classes supplémentaires à l'aide d'addActionListener()
.
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ée Bouton3Listener
). Implémentez-y l'interface ActionListener
et contentez-vous d'effectuer un simple System.out.println()
dans la méthode actionPerformed()
. 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é :
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 | //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.
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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'objet CardLayout
: c'étaient des classes anonymes. Pour rappel, voici ce que je vous avais amenés à coder :
1 2 3 4 5 6 7 | 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'instruction new 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.
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 :
1 2 3 4 5 6 7 8 9 10 11 | 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ées
abstract
; - elles ne peuvent pas non plus être déclarées
static
; - elles ne peuvent pas définir de constructeur ;
- elles sont automatiquement déclarées
final
: 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 classe Fenetre
. Il va falloir changer le libellé des boutons de notre IHM : le premier affichera Go
et le deuxième Stop
. 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 bouton
Go
ne sera pas cliquable alors que le boutonStop
oui ; - si l'animation est interrompue, le bouton
Stop
ne sera plus cliquable, mais le boutonGo
le sera.
Ne vous inquiétez pas, c'est très simple à réaliser. Il existe une méthode gérant ces changements d'état :
1 2 3 | 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éthode setSize()
, mais de la méthode setPreferredSize()
. Elle prend en paramètre un objet Dimension
, qui, lui, prend deux entiers comme arguments.
Voici un exemple :
1 | bouton.setPreferredSize(new Dimension(150, 120)); |
En l'utilisant dans notre application, nous obtenons la figure suivante.
Afin de bien gérer notre animation, nous devons améliorer notre méthode go()
. 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 classe Fenetre
.
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | 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 bouton
Go
n'est pas cliquable et l'autre l'est ; - l'animation se lance ;
- l'animation s'arrête lorsque l'on clique sur le bouton
Stop
; - le bouton
Go
devient 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 objet Fenetre
. 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éthode actionPerformed()
appelle la méthode go()
et, étant donné que nous nous trouvons à l'intérieur d'une boucle infinie, nous restons dans la méthode go()
et ne sortons plus de la méthode actionPerformed()
.
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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.
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.
Lors de tous les appels, on dit que la JVM empile les invocations sur la pile. Une fois que la dernière méthode empilée a terminé de s'exécuter, la JVM la dépile.
La figure suivante présente un schéma résumant la situation.
Dans notre programme, imaginez que la méthode actionPerformed()
soit représentée par la méthode 2, et que notre méthode go()
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éthode go()
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 bouton Stop
, 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 pattern Observer
est 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 de Swing
et AWT
. 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é.
Package com.sdz.vue, classe Fenetre.java
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 | 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); } } |
Package com.sdz.model, classe Horloge.java
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 | 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(); } } } } |
Si vous ne disposez pas de la police d'écriture que j'ai utilisée, utilisez-en une autre : Arial
ou Courrier
, par exemple.
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 de JLabel
dans son objet Horloge
, 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 IHM swing
à une IHM awt
.
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'objet Horloge
dans une autre application, vous serez confrontés à plusieurs problèmes étant donné que cet objet ne s'affiche que dans un JLabel
.
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).
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'objet Observateur
. 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.
Ce diagramme indique que ce ne sont pas les instances d'Horloge
ou de JLabel
que 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 classe Fenetre
implémentera l'interface Observateur
: nous pourrons la voir comme une classe du type Observateur
. Vous avez sans doute remarqué que la deuxième interface - celle dédiée à l'objet Horloge
- 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 type Observateur
.
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 classe
Horloge
dans notre classeFenetre
; - cette dernière implémentera l'interface
Observateur
; - notre objet
Horloge
, 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 objet
Horloge
fera 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 package com.sdz.observer
).
Observateur.java
1 2 3 4 5 | package com.sdz.observer; public interface Observateur { public void update(String hour); } |
Observer.java
1 2 3 4 5 6 7 | 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
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 | 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
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 | 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éthode updateObservateur()
est invoquée. Celle-ci parcourt la collection d'objets Observateur
et appelle sa méthode update(String hour)
. La méthode étant redéfinie dans notre classe Fenetre
afin de mettre JLabel
à 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 abstraite Observable
et une interface Observer
fournies 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 classe Observable
. 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.
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éthode update(Observable obs, Object obj)
: sa signature a changé.
Cette méthode prend ainsi deux paramètres :
- un objet
Observable
; - un
Object
repré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 :
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 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'interface
MouseListener
. - 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 est
ActionListener
issue du packagejava.awt
. - La méthode à implémenter de cette interface est
actionPerformed()
. - 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.