La programmation événementielle

Chapitres traités   

La gestion des événements est d'une importance capitale pour les programmes ayant une interface utilisateur graphique. En effet, en mode graphique, ce n'est plus une programmation déterministe, qui impose donc un fonctionnement séquentiel, mais plutôt une programmation qui propose un ensemble d'actions spécifiques, au moment où l'utilisateur le désire et en rapport avec des événements particuliers, comme par exemple le clic d'un bouton.

Cette étude explique le fonctionnement du modèle d'événement AWT. Vous apprendrez à capturer des événements en provenance de la souris et du clavier, ainsi que l'usage des éléments les plus simples d'une interface, comme les boutons. Nous verrons en particulier comment utiliser les événements de base générés par ces composants.

Pour appréhender ce cours, il est nécessaire de maîtriser la notion des interfaces, mais aussi l'utilisation des classes inernes et des classes anonymes. Revoir les interfaces. Revoir les classes internes et anonymes.

Choix des chapitresIntroduction à la gestion des événements

Tout système d'exploitation qui supporte des interfaces graphiques doit constamment surveiller l'environnement afin de détecter des événements tels que la pression sur une touche du clavier ou sur un bouton de la souris. Le système d'exploitation en informe alors les programmes en cours d'exécution. Chaque programme détermine ensuite s'il doit répondre à ces événements.

La programmation événementielle constitue la caractéristique essentielle d'une interface graphique. La plupart des événements sont créés par des composants que nous auront introduit dans la fenêtre, comme les menus, les boutons, les boîtes de dailoques, etc. Nous allons ainsi voir comment traiter les événements qu'ils génèrent.

L'environnement de programmation Java a choisi une approche particulière de la gestion événementielle. En effet, dans les limites des événements connus d'AWT, vous contrôlez complètement la manière dont les événements sont transmis de la source (par exemple, un bouton ou une barre de défilement) à l'écouteur (celui qui va capturer et gérer l'événement).

Vous pouvez désigner n'importe quel objet comme écouteur d'événement, c'est ce qui fait d'ailleurs la particularité de Java. Dans la pratique, vous choisirez un objet écouteur qui soit capable de fournir une réponse appropriée à l'événement.

Les sources d'événement possèdent des méthodes addXXXListener() qui leur permettent d'enregistrer (ou de recenser) les écouteurs d'événement sélectionnés XXXListener. Lorsqu'un événement arrive à la source, celle-ci envoie une notification à tous les objets écouteurs recensés pour cet événement.

Bien entendu, dans un langage orienté objet comme Java, l'information relative à l'événement est encapsulée dans un objet événement. Tous les objets événement dérivent, directement ou indirectement, de la classe java.util.EventObject. Il existe évidemment des sous-classes pour chaque type d'événement, comme ActionEvent et WindowEvent.

Pour résumer, voici en gros comment les événements sont gérer par l'AWT :

  1. Un objet écouteur est une instance d'une classe qui implémente une interface spéciale appelée interface écouteur (listener interface).
  2. Une source d'événement est un objet qui est capable de recenser des objets écouteurs et leur envoyer des objets événements.
  3. Lorsqu'un événement se produit, la source d'événement envoie l'objet événement à tous les écouteurs recensés.
  4. Les objets écouteurs utilisent alors l'information contenue dans l'objet événement pour déterminer leur réponse.

Pour recenser l'objet écouteur auprès de l'objet source, utiliser une instruction construite sur ce modèle :

ObjetSourceEvénement.addEvénementListener(objetEcouteurEvénement)

Voici un exemple :

ActionListener écouteur = ...;
JButton bouton = new JButton("Ok");
bouton.addActionListener(écouteur);

L'exemple précédent exige que la classe à laquelle appartient l'objet écouteur implémente l'interface appropriée (en l'occurence, ActionListener). Comme pour toutes les interfaces Java, l'implémentation d'une interface signifie qu'il faut redéfinir les méthodes prévues (respectant ainsi le contrat) avec la signature correcte. Ainsi, pour implémenter l'interface ActionListener, la classe de l'écouteur doit posséder une méthode nommée actionPerformed() qui recevra l'objet ActionEvent comme paramètre :
class Ecouteur implements ActionListener {
   ...
   public void actionPerformed(ActionEvent événement) {
       // traitement particulier à la réaction d'un clic sur le bouton
       ...
   }
}

Chaque fois que l'utilisateur clique sur le bouton, l'objet bouton de type JButton crée un objet événement de type ActionEvent et appelle la méthode écouteur.actionPerformed(événement) en lui passant cet objet. Il est possible d'ajouter plusieurs objets en tant qu'écouteurs d'une source d'événement, par exemple un bouton. Dans ce cas, le bouton appelle les méthodes actionPerformed() de tous les écouteurs, chaque fois que l'utilisateur clique sur ce bouton.

 

Choix du chapitre Apprentissage au travers de deux petits exemples

Afin d'illustrer cette entrée en matière, je vous propose de mettre en oeuvre deux petits programmes, un qui permet de prendre en compte un événement de type Action, et un autre qui gère les événements liés à la souris.

Interface ActionListener

Dans un premier temps, et en reprenant l'exemple du clic sur un bouton avec un écouteur de type ActionListener, nous allons mettre en oeuvre le petit programme que nous avons déjà vu, en version simplifiée, qui réalise la conversion entre des €uros et des francs :

 1 package conversion;
 2 
 3 import java.awt.*;
 4 import java.awt.event.*;
 5 import javax.swing.*;
 6 
 7 public class Conversion extends JFrame implements ActionListener {
 8    private JTextField saisie = new JTextField("0");
 9    private JButton conversion = new JButton("Conversion");
10    private JLabel résultat = new JLabel("0 Franc");
11    private JPanel panneau = new JPanel();
12    
13    public Conversion() {
14       setTitle("Conversion €uros -> Francs");
15       setBounds(100, 100, 280, 80);
16       setDefaultCloseOperation(EXIT_ON_CLOSE);
17       panneau.setLayout(new BorderLayout());
18       saisie.setHorizontalAlignment(JTextField.RIGHT);
19       panneau.add(saisie);
20       conversion.addActionListener(this);
21       panneau.add(conversion, BorderLayout.EAST);
22       add(panneau, BorderLayout.NORTH);
23       résultat.setHorizontalAlignment(JLabel.RIGHT);  
24       résultat.setBorder(BorderFactory.createEtchedBorder());
25       add(résultat, BorderLayout.SOUTH);
26       getContentPane().setBackground(Color.GREEN);
27       setResizable(false);
28       setVisible(true);
29    }
30    
31    public static void main(String[] args) {
32       new Conversion();  
33    }
34 
35    public void actionPerformed(ActionEvent e) {
36        final double TAUX = 6.55957;
37        double €uro = Double.parseDouble(saisie.getText());
38        double franc = €uro * TAUX;
39        résultat.setText(franc+" Francs");
40    }
41 }
42 

L'interface ActionListener utilisée dans notre exemple n'est pas limitée aux clics sur des boutons. Elle peut être utilisée dans bien d'autres situations, par exemple :

  1. Lors de la sélection d'un élément de menu.
  2. Lors de la sélection d'un élément dans une zone de liste à l'aide d'un double-clic.
  3. Lorsque la touche "Entrée" est activée dans un champ de texte. A titre d'exemple, dans le code précédent, vous pouvez rajouter cette ligne :

    saisie.addActionListener(this);
  4. Lorsqu'un composant Timer déclenche une impulsion après l'écoulement d'un temps donné.

En résumé, et quel que soit le contexte, ActionListener s'utilise de la même manière dans tous les situations que nous venons d'évoquer ; sa méthode (unique) actionPerformed() reçoit en paramètre un objet de type ActionEvent. Cet objet fournit des informations sur l'événement qui a été déclenché.

La question qui se pose constamment, c'est le choix de l'écouteur. Java est très souple à ce sujet. Vous pouvez créer vos propres classes ou utiliser celles qui sont déjà présentes. Le tout, c'est que la ou les méthodes relatives aux événements puissent réaliser le traitement souhaité. Ici, il est indispensable de pouvoir atteindre à la fois l'objet saisie et l'objet résultat, puisque le traitement à réaliser est en relation avec ces deux éléments. Vu que nous avons qu'un seul bouton qui provoque l'événement, il me paraît judicieux que ce soit la fenêtre elle-même qui soit écouteur de ce type d'événement puisqu'elle dispose des éléments que nous venons d'évoquer et qui vont donc servir au traitement. Il est tout à fait possible également de créer une nouvelle classe spécifique à la gestion de cet événement mais, pour qu'elle puisse accéder aux objets saisie et résultat, il faut que ce soit impérativement une classe interne. Ce sujet sera traiter ultérieurement.

Interface MouseListener

Voyons également comment traiter l'événement que constitue un clic sur une surface (objet issu de JPanel) qui se trouve sur la zone principale de la fenêtre. Nous nous contenterons de signaler l'événement en affichant les coordonnées de la souris sur la partie basse de la fenêtre (coordonnées objet de la classe Coordonnées qui hérite de JLabel).

Nous allons appliquer la même démarche que précédemment en recensant tous les éléments nécessaires à la gestion complète de l'événement choisi, savoir :

  1. En java, je le rappelle, tout événement possède ce que l'on nomme une source. Il s'agit de l'objet lui ayant donné naissance, comme un bouton, un article de menu, une fenêtre, un panneau, etc. Dans notre exemple, cette source est la surface de travail.
  2. Pour traiter un événement, nous devons associer à la source un objet de son choix dont la classe implémente une interface particulière correspondant à une catégorie d'événements. On dit que cet objet est un écouteur de cette catégorie d'événements. Chaque méthode proposée par l'interface correspond à un événement particulier de la catégorie choisie.
  3. Ainsi, il existe une catégorie d'événements souris que nous pouvons traiter à l'aide d'un écouteur de souris, c'est à dire un objet d'une classe implémentant l'interface MouseListener. Cette dernière comporte cinq méthodes correspondant chacune à un événement particulier :
    1. mousePressed() : appuie sur un bouton de la souris.
    2. mouseReleased() : relâchement de l'action sur un bouton de la souris.
    3. mouseClicked() : clic sur un bouton de la souris qui correspond en réalité à un appuie suivie d'un relâchement.
    4. mouseEntered() : le curseur de la souris passe au dessus de l'élément source.
    5. mouseExited() : le curseur sort de la zone prise par l'élément source.
  4. Une classe susceptible d'instancier un objet écouteur de ces différents événements devra donc correspondre à ce schéma :

L'événement qui nous intéresse ici correspond à un clic usuel (appui suivi de relâchement, sans déplacement). Nous devrons donc proposer une classe écouteur qui redéfinie la méthode mouseClicked() afin de réaliser le traitement souhaité. Mais, comme notre classe doit implémenter l'interface MouseListener, vous remarquez qu'elle doit également redéfinir toutes les autres méthodes (contrat à respecter par rapport à une interface). Nous pouvons toutefois nous permettre de ne rien faire de particulier pour toutes ces méthodes supplémentaires non utiles pour notre application. La définition de ces méthodes sera donc "vide".

Pour traiter un clic de la souris dans la surface de travail de la fenêtre, il suffit d'associer à notre surface un objet d'un type tel que EcouteurSouris. Pour ce faire, nous utilisons la méthode addMouseListener() (le nom de la méthode est évocateur puisque qu'il est associé au type d'écouteur - add+MouseListener) :

sourceEvénement.addMouseListener(objetEcouteur);

dans laquelle objetEcouteur est un objet d'une classe du type EcouteurSouris dont nous venons de fournir le schéma et sourceEvénement correspond à notre surface de travail.

Voici le code source correspond à notre analyse :

Encore une fois, je propose un écouteur sur un composant graphique. Toutefois, je crée une nouvelle classe qui hérite d'un JLabel et qui implémente donc l'interface MouseListener. L'intérêt ici, c'est de pouvoir utiliser la méthode setText() inhérente d'un JLabel et de proposer ainsi l'affichage des coordonnées de la souris

Choix de l'objet écouteur

Comme je l'ai déjà évoqué, Java se montre très souple puisque l'objet écouteur peut être n'importe quel objet dont la classe implémente l'interface voulue. Dans une situation aussi simple qu'ici, nous pouvons même ne pas créer de classe séparée telle que Coordonnées en faisant de la fenêtre elle-même son propre écouteur d'événement souris. Notez que cela est possible car la seule chose que nous demandons à un objet écouteur est que sa classe implémente l'interface voulue (ici MouseListener).

Utilisation de l'information associé à un événement

Préoccupons nous maintenant de l'arguement transmis à la méthode mouseClicked(). Ici, il s'agit d'un objet de type MouseEvent. Cette classe correspond en fait à la catégorie d'événements gérés par l'interface MouseListener. Un objet de cette classe est automatiquement créé par Java lors du clic, et transmis à l'écouteur voulu. Il contient un certain nombre d'informations, en particulier les coordonnées du curseur de la souris au moment du clic, lesquelles sont accessibles, respectivement, par les méthodes getX() et getY().

 

Choix du chapitre La notion d'adaptateur

Dans les exemples précédents, nous n'avions besoin que d'une seule méthode, la méthode mouseClicked(). Toutefois, pour respecter le contrat prévu, nous avons dû fournir des définitions vides pour toutes les autres méthodes requises afin d'implémenter correctement l'interface MouseListener.

Une classe qui implémente une interface doit obligatoirement tenir la promesse de définir chacune des méthodes présentes dans l'interface.
.

il est fastidieux d'écrire des signatures de quatre méthodes qui ne font rien. Pour simplifier la tâche du programmeur, chacune des interfaces AWT possédant plusieurs méthodes est accompagnée d'une classe adaptateur qui implémente toutes les méthodes de l'interface en leur attribuant des instructions vides. Par exemple, la classe MouseAdapter possède cinq méthodes qui ne font rien.



Cela signifie que la classe adpatateur satisfait automatiquement aux exigences techniques imposées par Java pour l'implémentation de l'interface écouteur qui lui est associée. Nous pouvons ainsi étendre la classe adaptateur afin de spécifier les réactions souhaités pour certains événements, mais sans avoir besoin de répondre explicitement à tous les événements de l'interface.

Toutefois, une interface comme ActionListener, qui ne possède qu'une seule méthode, n'a pas besoin de classe adaptateur.
.

Profitons de cette caractéristique et utilisons l'adaptateur de la souris. Nous pouvons ainsi étendre la classe MouseAdapter, héritant ainsi de cinq méthodes qui ne font rien, et nous contenter de surcharger la méthode mouseClicked() :

Voici un schéma récaptitulatif montrant comment utiliser cette technique pour n'écouter, à l'aide d'un objet d'une classe EcouteurSouris, que les clics complets générés par une fenêtre (la classe MaFenêtre devient source des événements) :

Cependant, si l'on procède ainsi, les deux classes MaFenêtre et EcouteurSouris sont indépendant. Dans certains programmes, on préferera que la fenêtre concernée soit son propre écouteur. Dans ce cas, un petit problème se pose : la classe fenêtre correspondante ne peut pas dériver à la fois de JFrame et de MouseAdapter. C'est là que la notion de classe anonyme prend tout son intérêt. Il suffit en effet de remplacer le canevas précédent par le suivant :

Ici, nous créons un objet d'un type classe anonyme dérivée de MouseAdapter et dans laquelle nous redéfinissons de façon appropriée la méthode mouseClicked(). Du coup, voilà comment transformer notre programme précédant pour tenir compte de toutes ces considérations :

 

Choix des chapitres La gestion des événements en général

Nous venons de voir comment un événement, déclenché par un objet nommé source, pouvait être traité par un autre objet nommé écouteur préalablement associé à la source. Tout ce qui a été exposé ici, sur deux exemples simples, se généralisera aux autres événements, quels qu'ils soient et quelle que soit leur source.

En particulier, nous associerons toujours un objet écouteur à un événement d'une catégorie donnée ##Listener par une méthode add##Listener(). Chaque fois qu'une catégorie donnée disposera de plusieurs méthodes, nous pourrons :

  1. soit redéfinir toutes ces méthodes, certaines ayant éventuellement un corps vide,
  2. soit faire appel à une classe dérivée d'une classe adaptateur - ##Adapter - et ne fournir que les méthodes qui nous intéressent.

L'objet écouteur pourra être n'importe quel objet de votre choix ; en particulier, il pourra s'agir de l'objet source lui-même. Enfin, bien que nous n'ayons pas rencontré ce cas jusqu'ici, sachez qu'un même événement peut tout à fait disposer de plusieurs écouteurs.

  1. Les sources d'événement sont des composants de l'interface utilisateur, des fenêtres et des menus.
  2. Le système d'exploitation notifie à une source d'événement, les activités qui peuvent l'intéresser, comme les mouvements de la souris ou les frappes du clavier.
  3. La source d'événement décrit la nature de l'événement dans un objet événement - ##Event. Elle stocke également une liste d'écouteur - ##Listener - des objets qui souhaitent être prévenus quand l'événement se produit.
  4. La source d'événement appelle la méthode appropriée de l'interface écouteur afin de fournir des informations sur l'événement aux divers écouteurs recencés. Pour cela, la source passe l'événement objet adéquat à la méthode de la classe écouteur.
  5. L'écouteur analyse l'objet événement pour obtenir de informations détaillées. Par exemple, nous pouvons utiliser la méthode getSource() pour connaître la source, ou les méthodes getX() et getY() de la classe MouseEvent pour connaître la position courante de la souris.

 

Choix du chapitre Choix de l'objet écouteur

Nous verrons par la suite qu'il existe toute sorte d'événements. Avant de les découvrir, j'aimerais que nous nous consacrions de nouveau sur le choix de ou des écouteurs. Nous avons déjà eu une petite approche, mais jusqu'à présent, nous n'avions qu'une seule source d'événement. Que se passe-t-il si nous devons gérer plusieurs sources d'événement pour une même destination (traitements différents pour le même composant) ?

En réalité, beaucoup de solutions sont envisageables. Nous allons tenter d'en découvrir quelles unes au travers d'un même exemple. Nous allons effectivement mettre en oeuvre un programme qui permet de changer la couleur de la surface de travail de la fenêtre de votre application en cliquant sur des boutons adaptés - ou en appuyant sur la touche Espace du clavier lorsque le bouton choisi comporte le focus d'entrée ; c'est le cas sur cet exemple avec le bouton Cyan (Rectangle bleu). Nous nous servons encore une fois de l'interface ActionListener qui correspond à notre désir et qui comporte une seule méthode actionPerformed(). Dans ce cas de figure, l'objet notifié est un ActionEvent. Quelque soit l'exemple étudié, les sources des événements seront toujours les deux boutons.

La surface de travail de la fenêtre est l'écouteur des actions proposées sur les deux boutons

Pour ce premier exemple, la surface de travail panneau qui est issue de la classe Panneau est à l'écoute de deux événements possibles qui correspondent aux actions sur les boutons boutonCyan et boutonMagenta. Nous utilisons ensuite la méthode getActionCommand() pour récupérer le libellé du bouton qui a provoqué l'événement et ainsi pour proposer la couleur correspondante.

Le choix de cette méthode getActionCommand() n'est pas des plus heureux. En effet, pour une raison quelconque, nous pouvons changer le texte du libellé du bouton. Dans ce cas là, les événements seraient alors mal gérés. Eviter, si possible, d'utiliser cette démarche.

La fenêtre est l'écouteur des actions proposées sur les deux boutons

Cette fois-ci, c'est la fenêtre de l'application qui fait office d'écoute des événements proposés par l'action sur l'un des deux boutons. Il n'est pas nécessaire, dans ce cas là, de fabriquer une classe spéciale Panneau. Puisque depuis la fenêtre, il est possible de connaître les boutons qui sont à l'origine de l'action, il suffit de récupérer les objets représentatif grâce à la méthode getSource().

Chacun des boutons est à la fois source et écouteur

Nous allons cette fois-ci créer une classe abstraite Bouton qui hérite de JButton et qui implémente l'interface ActionListener, ce qui sous-entend que nous créons un bouton qui est à la fois la source et écouteur de son propre événement ActionEvent. Nous allons ensuite créer chaque bouton - par le système des classes anonymes - qui implémentera cette classe et qui proposera en conséquence l'événement correspondant. La classe Bouton est abstraite puisque à son niveau, il n'est pas encore possible de redéfinir la méthode actionPerformed().

L'écouteur est totalement indépendant de tout composant graphique

Cette fois-ci, nous créons un écouteur de toute de pièce, indépendamment de tout composant existant. Dans ce cas de figure, il n'existe aucun héritage. Dans cet exemple, ce choix ne me paraît pas judiceux. Toutefois, certaines situations peuvent nécessiter de fabriquer un écouteur sans héritage.

L'écouteur est toujours indépendant de tout composant graphique, mais c'est aussi une classe interne

En java, il est possible de déclarer une classe à l'intérieur d'une autre. Même si cette technique n'est pas fréquente, elle présente l'avantage de donner la possiblité d'accéder aux attributs de la classe conteneur depuis la classe interne. C'est une technique qu'il ne faut pas avoir peur d'utiliser. Elle offre effectivement beaucoup de souplesses.

Peut-être la meilleure solution - le bouton un objet interne qui est à la fois source et écouteur

Pour finir, je vous propose le code source suivant, qui allie les différentes opportunités, en prenant les avantages de chacune des techniques envisagées. Le meilleurs choix est souvent la classe interne puisqu'elle accède à tous les éléments de la classe conteneur. Egalement, le fait de travailler avec le composant source, vous pouvez faire appel, une fois pour toute dans le constructeur, à la méthode addXXXListener(). Enfin, le traitement est simplifié par l'ajout d'un attribut interne qui prend en compte la valeur à transmettre pour l'action à lancer. Encore une fois, le fait d'avoir une classe interne permet de réaliser le traitement sur l'élément souhaité.

package événement;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private Bouton cyan = new Bouton("Cyan", Color.CYAN);
   private Bouton magenta = new Bouton("Magenta", Color.MAGENTA);
   private JPanel panneau = new JPanel();
   
   public Fenêtre() {
      super("Les événements");
      setSize(300, 250);
      setLocation(50, 20);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      panneau.add(cyan);
      panneau.add(magenta);
      add(panneau, BorderLayout.SOUTH);
      setVisible(true);
   }
   
   public static void main(String[] args) {
      new Fenêtre();        
   }
   
   private class Bouton extends JButton implements ActionListener {
      private Color couleur;

      public Bouton(String libellé, Color couleur) {
         super(libellé);
         this.couleur = couleur;
         addActionListener(this);
      }

      public void actionPerformed(ActionEvent e) {
         getContentPane().setBackground(couleur);
      }     
   }
}

Imaginez qu'au lieu de deux boutons, vous en ayez cinq. Le fait de factoriser dans une seule classe interne, tout ce qui correspond à la gestion des événements, est vraiment avantageux.

Il existe bien d'autres possibilités. Il suffit d'avoir un peu d'imagination. Java est un langage qui offre beaucoup de souplesse.
.

 

Choix du chapitre Recensement des différents types d'événement

Maintenant que nous connaissons toute la technique concernant la gestion des événements, nous allons recenser l'ensemble des événements que Java propose. Tous les événements utilisés par les composants graphiques de Swing sont des classe filles de java.util.EventObject. Un objet événement encapsule des informations sur l'événement que la source d'événement communique aux écouteurs, comme nous l'avons fait à l'aide des méthodes getSource() et getActionCommand() dans les exemples précédents.

Evénements sémantiques et de bas niveau

AWT fait une distinction utile entre événements de bas niveau et événements sémantiques :

  1. Un événement sémantique exprime ce que fait l'utilisateur, par exemple "Cliquer sur un bouton" ; en conséquence, un événement ActionEvent est sémantique.
  2. Les événements de bas niveau sont ceux qui rendent l'action possible. Dans le cas d'un clic sur un bouton de l'interface utilisateur, cela représente en réalité une pression sur le bouton de la souris, des déplacements du pointeur de la souris, puis un relâchement du bouton de la souris (mais seulement si le pointeur se trouve encore dans la zone du bouton affiché à l'écran). Ce peut être également une pression sur une touche du clavier, au cas où l'utilisateur sélectionne le bouton avec la touche de tabulation puis active la barre d'espacement.

De la même manière, un ajustement de la position d'une barre de défilement est un événement sémantique, mais le déplacement de la souris est un événement de bas niveau.

Classes d'événements sémantiques les plus utilisés dans le paquetage java.awt.event
  1. ActionEvent : pour un clic de bouton, une sélection d'un élement de menu ou de liste, ou une pression de la touche "Entrée" dans un champ de texte.
  2. AdjustementEvent : l'utilisateur déplace le curseur d'une barre de défilement.
  3. ItemEvent : l'utilisateur fait une sélection dans un groupe de cases à cocher ou dans une liste.
Classes d'événement de bas niveau
  1. KeyEvent : une touche du clavier est enfoncée ou relâchée.
  2. MouseEvent : le bouton de la souris est enfoncée ou relâchée ; le pointeur de la souris se déplace ou glisse.
  3. MouseWheelEvent : la mollete de la souris tourne.
  4. FocusEvent : un composant obtient le focus.
  5. WindowsEvent : l'état de la fenêtre change.

Tous les événements de bas niveau héritent de ComponentEvent. Cette classe dispose d'une méthode, nommée getComponent(), qui indique le composant ayant généré l'événement ; vous pouvez employer getComponent() à la place de getSource(). En effet, la méthode getComponent() renvoie la même valeur que getSource(), mais l'a déjà transtypée en Component. Par exemple, si un événement clavier a été déclenché à la suite d'une frappe dans un champ de texte, getComponent() renvoie une référence à ce champ de texte.

getComponent() <=> (Component)getSource()

Ecouteurs d'événement

Les interfaces suivantes permettent d'écouter ces événements :

  1. ActionListener
  2. AjustementListener
  3. FocusListener
  4. ItemListener
  5. KeyListener
  6. MouseListener
  7. MouseMotionListener
  8. MouseWheelListener
  9. WindowsListener
  10. WindowsFocusListener
  11. WindowsStateListener

Vous remarquez la présence de deux interfaces séparées pour la souris, pour des raisons d'efficacité : MouseListener et MouseMoveListener. La deuxième est plutôt spécialisée au mouvement de la souris. Il se produit de nombreux événements lorsque l'uitlisateur déplace la souris. Un écouteur qui s'intéresse uniquement aux clics de la souris ne doit pas être inutilement prévenu de tous les déplacements de la souris.

Classes adaptateur

Plusieurs interfaces écouteur AWT - celles qui possèdent plusieurs méthodes - sont accompagnées d'une classe adaptateur qui implémente toutes les méthodes de l'interface afin qu'elles n'accomplissent aucune action (les autres interfaces n'ont qu'une seule méthode et il est donc inutile d'employer des classes adaptateur dans ce cas).

Voici les classes adaptateur les plus souvent utilisées :

  1. FocusAdapter
  2. KeyAdapter
  3. MouseAdapter
  4. MouseMotionAdapter
  5. WindowAdapter

Il faut manifestement connaître un grand nombre de classes et d'interfaces - ce qui peut paraître insurmontable à première vue. Heureusement, le principe est simple. Une classe qui désire recevoir des événements doit impléménter une interface écouteur. Elle se recense auprès de la source d'événement, puis elle reçoit les événements souhaités et les traite grâce aux méthodes de l'interface écouteur.

Ensemble des éléments employés pour la gestion des événements

Nous allons ici recenser les objets événements les plus utilisés avec leurs méthodes associées suivi des interfaces qui les prennent en compte pourvues également de méthodes spécifiques aux différents traitements souhaités.

Objets événements
EventObject
Généré par tous les composants.
Objet parent de tous les objets événement.
getSource() : Renvoie une référence sur l'objet qui a déclenché l'événement.
InputEvent
Généré par tous les composants.
Objet parent de KeyEvent et de MouseEvent.
isAltDown(), isControlDown(), isMetaDown(), isShiftDown() : Ces méthodes renvoient true si la touche de modification correspondante est pressée lorsque l'événement est généré.
FocusEvent
Généré par Component.
Un composant a obtenu ou perdu le focus.
isTempory() : indique si le changement de focus est temporaire ou permanent.
getOppositeComponent() : Renvoie le composant qui a perdu le focus dans le gestionnaire focusGained ou le composant qui a obtenu le focus dans le gestionnaire focusLost.
ComponentEvent
Généré par tous les composants.
Tous les événements de bas niveau héritent de cette classe.
getComponent() : indique le composant ayant généré l'événement.
ActionEvent
Généré par AbstractButton, JComboxBox, JTextField, Timer.
Pour un clic de bouton, une sélection d'un élément de menu ou de liste, une pression de la touche Entrée dans un champ de texte.
getActionCommand()
: renvoie la chaîne de commande associée à cet événement d'action. Si cet événement est déclenché par un bouton, la chaîne de commande contient le libellé du bouton, à moins qu'il n'ait été modifié par la méthode setActionCommand().
getModifiers()
: renvoie une valeur qui indique les modificateurs clavier - alt, ctrl, shift - qui sont effectifs quand l'événement de d'action est déclenché.
AdjustementEvent
Généré par JScrollBar.
L'utilisateur a déplacé le curseur d'une barre de défilement.
getAjustable() : retourne l'objet qui a provoqué l'événement.
getAdjustementType() : indique comment se déroule le défilement. Voici les différentes valeurs retournées : UNIT_INCREMENT, UNIT_DECREMENT, BLOCK_INCREMENT, BLOCK_DECREMENT, TRACK.
getValue() : retourne la valeur courante de l'ascenceur.
ItemEvent
Généré par AbstractButton, JComboxBox.
L'utilisateur fait une sélection dans un groupe de case à cocher ou dans une liste.

getItem() : renvoie un objet représentant l'item qui a tété sélectionné ou désélestionné.
getItemSelectable() : est un remplacement commode à getSource(), et renvoie l'objet ItemSelectable qui a déclenché l'événement.
getStateChange() : renvoie le nouvel état de sélection de l'item : une des constante SELECTED ou DESELECTED.
KeyEvent
Généré par Component.
Une touche du clavier a été pressée ou relâchée.
getKeyChar() : Renvoie le caractère tapé par l'utilisateur.
getKeyCode() : renvoie le code de touche virtuel de cet élément du clavier - VK_0, VK_A, VK_LEFT, ...
getKeyModifiersText() : renvoie une chaîne décrivant les touches de modification comme shift ou ctrl+shift.
getKeyText() : Renvoie une chaîne décrivant le code de touche. Par exemple, getKeyText(KeyEvent.VK_END) renvoie "End" (ou "Fin" si votre version Java est localisée).
isActionKey() : renvoie true si la touche de cet événement est une touche "d'action" - Origine, Fin, Page précédente, Tabulation, etc.
MouseEvent
Généré par Component.
Le bouton de la souris a été enfoncé ou relâché ; le pointeur de la souris a été déplacé, ou glissé dans une opération de glisser-déplacer.
getX(), getY(), getPoint()
: Renvoie la coordonnée x, la coordonnée y et le point sur lequel s'est produit l'événement, dans le système de coordonnées de la source.
getClickCount()
: Renvoie le nombre de clics de souris consécutifs associés à l'événement.
translatePoint() : exécute un déplacement relatif des coordonnées de la souris.
isPopupTrigger() : renvoie true si le bouton concerné (généralement le droit) est celui traditionnellement réservé au menu surgissant.
MouseWheelEvent
Généré par Component.
La molette de la souris tourne.
getWheelRotation() : indique le nombre d'impulsions correspondant à la rotation de la molette.
getScrollAmount() : donne l'unité de mesure de chaque impulsion.
WindowEvent
Généré par Window.
La fenêtre a été activée, désactivée, réduite en icône, réouverte ou fermée.
getWindow() : détermine l'objet Window qui est la source de l'événement.
getOppositeWindow() : retourne la fenêtre qui possédait (anciennement) le focus.
getNewState(), getOldState() : Ces méthodes renvoient l'ancien état et le nouvel état d'une fenêtre dans un événement de modification de l'état de fenêtre. L'entier renvoyé correspond à l'une des valeurs suivantes : Frame.NORMAL, Frame.ICONIFIED, Frame.MAXIMIZED_HORIZ, Frame.MAXIMIZED_VERT, Frame.MAXIMIZED_BOTH.
Interfaces et classes adaptateur
ActionListener
Ecouteur qui prend en compte un choix ou une validation.
actionPerformed(ActionEvent) : traitement à réaliser après une validation venant de l'utilisateur.
AdjustementListener
Ecouteur qui prend en compte le déplacement du curseur d'une barre de défilement.
adjustmentValueChanged(AdjustementEvent) : traitement à réaliser après une modification du curseur de la barre de défilement.
ItemListener
Ecouteur qui prend en compte la sélection dans un groupe de cases à cocher ou dans une liste.
itemStateChanged(ItemtEvent)
: traitement à réaliser après la sélection.
FocusListener - FocusAdapter
Ecouteur qui prend en compte la sélection dans un groupe de cases à cocher ou dans une liste.
focusGained(FocusEvent) : traitement à réaliser avec l'obtention du focus.
focusLost(FocusEvent) : traitement à réaliser après la perte du focus.
KeyListener - KeyAdepter
Ecouteur qui prend en compte le clavier.
keyPressed(KeyEvent) : une touche du clavier a été pressée.
keyReleased(KeyEvent) : une touche du clavier a été relâchée.
keyTyped(KeyEvent) : appelée (en plus des deux précédentes), lorsque le caractère est définitevement saisie, avec la prise en compte des modificateurs ou des touches de fonction ou encore des séquences de touches multiples.
MouseListener - MouseAdapter
Ecouteur qui prend en compte les événements venant de la souris (mais pas les événements liés aux mouvements de la souris).
mousePressed(
MouseEvent) : l'utilisateur a appuyer sur un bouton de la souris.
mouseReleased(MouseEvent) : l'utilisateur a relâché le bouton de la souris.
mouseEntered(MouseEvent) : le pointeur de la souris est rentré dans le composant.
mouseExited(MouseEvent) : Le pointeur de la souris est resorti du composant.
mouseCliqued(MouseEvent): clic complet de la souris avec un appui suivi du relâchement sans déplacement entre temps.
MouseMotionListener - MouseMotionAdapter
Ecouteur qui prend en compte les événements liés aux mouvements de la souris.
mouseDragged(MouseEvent)
: l'utilisateur a déplacé la souris en maintenant un bouton enfoncé.
mouseMoved(MouseEvent) : l'utilisateur a déplacé la souris sans tenir de bouton enfoncé.
MouseWheelListener
Ecouteur qui prend en compte les événements liés à la molette de la souris.
mouseWheelMoved(MouseWheelEvent) : traitements en relation avec la rotation de la molette de la souris.
WindowListener - WindowAdapter
Ecouteur qui prend en compte le changement d'état de la fenêtre.
windowClosing(WindowEvent) : l'utilisateur veut fermer la fenêtre par le menu système ainsi que par le bouton de fermeture. Cette fenêtre ne se fermera qu'avec un appel à sa méthode hide() ou dispose().
windowClosed(WindowEvent) : envoyé après qu'une fenêtre a été fermée par un appel à hide() (rendre non visible) ou dispose().
windowOpened(WindowEvent) : cette méthode est appelée lorsque la fenêtre a été ouverte.
windowIconified(WindowEvent) : la fenêtre est réduite (en icône ou en bouton).
windowDeiconified(WindowEvent): la fenêtre est restaurée (elle retrouve sa taille initiale alors qu'elle était réduite).
windowActivated(WindowEvent) : Envoyé quand la fenêtre est activée. La fenêtre active est généralement repérée par une barre de titre en surbrillance. Une seule fenêtre ne peut être active à un moment donné.
windowDeactivated(WindowEvent) : Envoyé quand la fenêtre cesse d'être la fenêtre active, typiquement quand l'utilisateur active une autre fenêtre.
WindowFocusListener - WindowFocusAdapter
Ecouteur qui prend en compte le changement de focus sur la fenêtre.
windowGainedFocus(WindowEvent) : traitement à réaliser avec l'obtention du focus ou un de ses éléments.
windowLostFocus(WindowEvent) : traitement à réaliser après la perte du focus.
WindowStateListener
Ecouteur qui prend en compte le changement d'état de la fenêtre (maximisée, réduite en icône ou restaurée à sa taille normale).
windowStateChanged(WindowEvent) : traitement à réaliser au changement d'état.

C'est loin d'être exhaustif, mais cela donne une idée de ce que nous pouvons réaliser. Il s'agit ici des éléments les plus souvent utilisés.
.

Choix du chapitre Mise en oeuvre d'un timer (minuteur)

Restons encore une fois sur l'écouteur ActionListener qui est bien utile également pour la gestion des timers. Le paquetage javax.swing contient une classe Timer, qui nous averti de l'expiration d'un délai imparti. Par exemple, si une partie de votre programme contient une horloge, vous pouvez demander à être averti à chaque seconde, de manière à pouvoir mettre à jour l'affichage de l'horloge.

Lorsque vous construisez un minuteur, vous définissez l'intervalle de temps et lui indiquez ce qu'il doit faire lorsque le délai est écoulé. Le traitement à réaliser doit être stipuler dans la méthode actionPerformed() de l'écouteur ActionListener que nous connaissons bien. Au moment où vous construisez votre minuteur, vous spécifier l'intervalle de temps en millièmes de seconde entre chaque notification ainsi que l'écouteur de type ActionListener que vous avez mis en oeuvre :

ActionListener écouteur = ...;
Timer minuteur = new Timer(1000, écouteur);

Une fois que votre minuteur est construit, vous pouvez le démarrer ou l'arrêter au moment où vous le désirez. Voici les méthodes respectives :

  1. start() : démarre le minuteur. La méthode actionPerformed() est alors sollicitée par le minuteur à chaque notification du délai écoulé.
  2. stop() : arrête le minuteur. Une fois arrêté, le minuteur n'appelle plus la méthode actionPerformed().

Si vous désirez que votre minuteur fonctionne, pensez bien à le démarrer au moyen de la méthode start().
.

Mise en oeuvre d'une horloge simple

Afin d'illustrer mes propos, je vous convie à réaliser un petit programme qui met en oeuvre une horloge simple à affichage digitale (sous forme textuelle).

package horloge;

import java.awt.*;
import java.awt.event.*;
import java.text.DateFormat;
import java.util.Date;
import javax.swing.*;

public class Fenêtre extends JFrame implements ActionListener {
   private Timer minuteur = new Timer(1000, this);
   private JLabel heure = new JLabel();
   
   public Fenêtre() {
      super("Horloge");
      setBounds(100, 100, 180, 80);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      heure.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32));
      heure.setHorizontalAlignment(JLabel.CENTER);
      add(heure);      
      minuteur.start();
      setResizable(false);
      setVisible(true);
   }
   
   public static void main(String[] args) {
      new Fenêtre();
   }

   public void actionPerformed(ActionEvent e) {
      heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
   }
}

 

Choix des chapitresCapture des événements de fenêtre

Dans un programme professionnel, il est souvent souhaitable de fermer l'application qu'après s'être assuré que l'utilisateur ne perdra pas son travail. Par exemple, vous pouvez souhaiter afficher une boîte de dialogue lorsque l'utilisateur ferme le cadre, pour l'avertir si un travail non sauvegardé risque d'être perdu, et ne sortir qu'après confirmation de l'utilisateur.

Lorsque l'utilisateur tente de fermer un cadre, l'objet JFrame est la source d'un événement WindowEvent. Nous devons donc avoir un objet écouteur approprié et l'ajouter à la liste des écouteurs de fenêtre :

WindowListener écouteur = ... ;
cadre.addWindowListener(écouteur);

L'écouteur de fenêtre doit être un objet d'une classe implémentant l'interface WindowListener, qui possède sept méthodes. Le cadre les appelle en réponse aux septs événements distincts qui peuvent se produire dans une fenêtre. Voici l'interface complète de WindowListener :

public interface WindowListener {
   void windowOpened(WindowEvent e);
   void windowClosing(WindowEvent e);
   void windowClosed(WindowEvent e);
   void windowIconified(WindowEvent e);
   void windowDeiconified(WindowEvent e);
   void windowActivated(WindowEvent e);
   void windowDeactivated(WindowEvent e);
}

Pour savoir si une fenêtre a été maximisée, il est préférable d'installer un WindowStateListener à la place d'un WindowListener.
.

La méthode qui m'intéresse ici est windowClosing(). Elle est effectivement appelée lorsque l'utilisateur clique sur le bouton de fermeture de la fenêtre :


Comme toujours en Java, toute classe qui implémente une interface doit implémenter toutes les méthodes de cette interface ; dans ce cas, cela signifie que les septs méthodes doivent être implémentées. Hors ici, une seule nous intéresse sur les septs prévues.

Nous pouvons, bien entendu définir, une classe qui implémente l'interface, ajouter les traitements spécifiques à la méthode windowClosing() et fournir des blocs vides pour les six autres méthodes. Toutefois, cette solution est fastidieuse. Nous avons vu qu'il existait des classes apaptateurs qui permettent ainsi de redéfinir uniquement les méthodes utiles à l'application. Voici deux codages de la même application avec ou sans classe anonyme :
package editeur;

import java.awt.event.*;
import java.io.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private JEditorPane édition = new JEditorPane();
   
   public Fenêtre() {
      super("Editeur de texte");
      setBounds(50, 50, 350, 250);
      add(new JScrollPane(édition));
      addWindowListener(new WindowAdapter() {
          public void windowClosing(WindowEvent e) {
             if (JOptionPane.showConfirmDialog(Fenêtre.this, "Sauvegardez votre travail")==JOptionPane.YES_OPTION) {
                try {
                   PrintWriter enregistrer = new PrintWriter("sauvegarde.txt");
                   enregistrer.println(édition.getText());
                   enregistrer.close();
                } 
                catch (FileNotFoundException ex) { } 
                System.exit(0);
             }           
          }
      });
      setVisible(true);
   }

   public static void main(String[] args) {
       new Fenêtre();
   }
}
  
package editeur;

import java.awt.event.*;
import java.io.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private JEditorPane édition = new JEditorPane();
   
   public Fenêtre() {
      super("Editeur de texte");
      setBounds(50, 50, 350, 250);
      add(new JScrollPane(édition));
      addWindowListener(new Ecouteur());
      setVisible(true);
   }
   
   public static void main(String[] args) {
      new Fenêtre();
   }
   
   private class Ecouteur extends WindowAdapter {
      public void windowClosing(WindowEvent e) {
          if (JOptionPane.showConfirmDialog(Fenêtre.this, "Sauvegardez votre travail")==JOptionPane.YES_OPTION) {
             try {
                PrintWriter enregistrer = new PrintWriter("sauvegarde.txt");
                enregistrer.println(édition.getText());
                enregistrer.close();
             } 
             catch (FileNotFoundException ex) { }              
          }   
          System.exit(0);
       }      
   }
}

Dans ce cas particulier, il faut bien penser à ne pas utiliser la méthode setDefaultCloseOperation(EXIT_ON_CLOSE).
.

 

Choix du chapitre Les événements du clavier

A partir de maintenant, nous allons examiner plus en détail les événements qui ne sont pas liés à des composants spécifiques, en particulier les événements relatifs au clavier et à la souris. Commençons donc par les événements issus du clavier.

La plupart du temps, vous n'avez pas à vous préoccuper du clavier car sa gestion est déjà assurée automatiquement par Java. C'est notamment le cas lors de la saisie d'un texte dans une boîte de saisie ou dans un champ de texte (les touches de correction telles que Return, Backspace, Insert, Delete, flèches droite ou gauche sont convenablement prises en compte).

Mais parfois, cette gestion automatique s'avère insuffisante. C'est notamment le cas si vous souhaitez desiner dans une fenêtre eu utilisant les touches du clavier ou encore si vous voulez afficher des caractères frappés au clavier. Nous allons voir ici comment procéder pour exploiter plus finement les événements correspondants.

Les événements générés

Les événements générés par le clavier appartiennent à la catégorie KeyEvent. Ils sont gérés par un écouteur implémentant l'interface KeyListener qui comporte trois méthodes :

  1. keyPressed() : appelée lorsqu'une touche est enfoncée.
  2. keyReleased() : appelée lorsqu'une touche est relâchée.
  3. keyTyped() : appelée (en plus des deux précédentes), lorsque le caractère est définitivement saisie, avec la prise en compte des modificateurs ou des touches de fonction ou encore des séquences de touches multiples.

Par exemple, la frappe du caractère A entraînera les appels suivants :

  1. keyPressed() : pour l'appui sur la touche Shift.
  2. keyPressed() : pour l'appui sur la touche a.
  3. keyReleased() : pour le relâchement de la touche a.
  4. keyReleased() : pour le relâchement de la touche Shift.
  5. keyTyped() : pour le caractère A.

En revanche, la frappe du caractère a (minuscule) n'entraîne que les appels suivants :

  1. keyPressed() : pour l'appui sur la touche a.
  2. keyReleased() : pour le relâchement de la touche a.
  3. keyTyped() : pour le caractère a.

Si nous nous contentons d'appuyer sur une touche telle que Alt et de la relâcher, nous obtiendrons seulement un appel de keyPressed(), suivi d'un appel de keyReleased(), sans aucun appel de keyTyped().

Vous pouvez ainsi suivre dans le moindre détail les actions de l'utilisateur sur le clavier. Bien entendu, si votre but est simplement de lire les caractères, vous pourrez vous contenter de ne traiter que les événements keyTyped().

Identification des touches

L'objet événement (de type keyEvent) reçu par les trois méthodes précédentes contient les informations nécessaires à l'identification de la touche physique du clavier ou du caractère concerné.

  1. D'une part, la méthode getKeyChar() fournit le caractère concerné (sous la forme d'une valeur de type char).
  2. D'autre part, la méthode getKeyCode() fournit un entier nommé code de touche virtuelle permettant d'identifier la touche physique du clavier. Il existe dans la classe KeyEvent un certain nombre de constantes correspondant à chacune des touches que nous pouvons rencontrer sur un clavier. Voici les principales :
  3. Enfin, il existe dans KeyEvent une méthode statique getKeyText() qui permet d'obtenir, sous la forme d'une chaîne, un bref texte expliquant le rôle d'une touche de code donné. Par exemple :

    String chaîne = KeyEvent.getKeyText(VK_SHIFT) ; // renvoie la chaîne "Shift".

Il est possible que certaines touches du clavier ne disposent pas de code de touche virtuelle. Dans ce cas Java fournit le code 0 (le texte associé est "Unknown keyCode : 0x0").

Lorsqu'une touche possède plusieurs significations (matérialisées par plusieurs gravures), elle ne dispose généralement que d'un seul code de touche virtuelle. Par exemple, sur un clavier francisé (AZERTY), la même touche comporte les trois gravures 3, " et #. Son code de touche sera toujours VK_3. Nous pouvons toutefois rencontrer quelques exceptions. Par exemple, sur un clavier doté d'un pavé numérique, la touche gravée 7 et flèche oblique fournira l'un des codes VK_NUMPAD7 ou VK_HOME selon que le clavier est vérouillé en numérique ou non.

Etat des touches modificatrices

Pour connaître l'état des touches Shift (Maj), Cntrl, Alt, Alt GR ou Meta, il est bien sûr possible d'intercepter les événements correspondants : VK_SHIFT, VK_CONTROL, VK_ALT, VK_ALT_GRAPH ou VK_META. Mais cette technique est ennuyeuse. Il est plus simple d'utiliser les méthodes spécifiques suivantes :

Source d'un événement clavier

Java considère qu'un événement clavier possède comme sources :

  1. Le composant ayant le focus au moment de l'action,
  2. les conteneurs éventuels de ce composant.

Nous voyons que tant que nous nous contentons d'intercepter les événements clavier dans la fenêtre principale, aucun problème ne se pose. Bien entendu, les composants comme les étiquettes (JLabel), qui ne peuvent pas recevoir de focus, ne pourront pas être la source de l'événement clavier, ce qui d'ailleurs n'aurait aucun sens.

En revanche, les composants comme les panneaux (JPanel) peuvent poser problème, car ils ne mettent pas en évidence leur focalisation. Il faudra alors appeler la méthode setFocusable() pour palier ce problème.

Mise en oeuvre de la gestion événementielle à partir du clavier - saisie confirmée par la touche Entrée (Validation)

Après toute cette étude technique, rentrons dans le vif du sujet en proposant un certain nombre d'exemples. Dans le premier, nous allons mettre en place une simple validation confirmée par la touche Entrée.

Ainsi, dans cet exemple, un message va être introduit dans la zone de saisie. Une fois que le message est écrit, nous confirmons la saisie en appuyant sur la touche Entrée du clavier. Dès lors, le même message apparaît sur la partie haute de la fenêtre, mais en majuscule. Comme il s'agit encore une fois d'une validation, nous utilisons de nouveau l'interface ActionListener et sa méthode associée actionPerformed().

package clavier;

import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;

public class Fenêtre extends JFrame implements ActionListener {
   private JTextField saisie = new JTextField();
   private JLabel message = new JLabel();
   
   public Fenêtre() {
      super("Evénéments du clavier");
      setSize(300, 200);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      saisie.addActionListener(this);
      add(saisie, BorderLayout.SOUTH);
      add(message, BorderLayout.NORTH);
      setVisible(true);
   }
   
   public static void main(String[] args) {
      new Fenêtre();  
   }

   public void actionPerformed(ActionEvent e) {
      message.setText(saisie.getText().toUpperCase());
   }
}

L'événement est capturé à la saisie de chaque caractère issu du clavier

Nous désirons, cette fois-ci, avoir le message qui se modifie instantanément après l'introduction de chaque caractère tapé sur le clavier. Par ailleurs, lorsque l'opérateur appuie sur la touche Entrée du clavier, le message s'efface aussi bien sur la zone de saisie que sur le partie haute de la fenêtre.

A priori, l'interface à utiliser pour ce type de comportement est cette fois-ci l'interface KeyListener. Toutefois, nous allons utiliser qu'une seule méthode parmi les trois proposées. Il est alors plutôt judicieux d'utiliser la classe adaptateur associée - KeyAdapter - et de passer ainsi par l'écriture d'une classe anonyme.

package clavier;

import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;

public class Fenêtre extends JFrame {
   private JTextField saisie = new JTextField();
   private JLabel message = new JLabel();
   
   public Fenêtre() {
      super("Evénéments du clavier");
      setSize(300, 200);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      add(saisie, BorderLayout.SOUTH);
      add(message, BorderLayout.NORTH);
      setVisible(true);
      saisie.addKeyListener(new KeyAdapter() {
         public void keyReleased(KeyEvent touche) {
             if (touche.getKeyCode()==KeyEvent.VK_ENTER) saisie.setText("");
             message.setText(saisie.getText().toUpperCase());
         }
      });
   }
   
   public static void main(String[] args) {
      new Fenêtre();  
   }
}

Si nous désirons que l'objet message soit une copie du texte de la zone de saisie, il est préférable de prendre la méthode keyReleased(). En effet, après le relâchement de la touche, le caractère saisi est déjà enregistré. La méthode keyPressed() correspond à l'événement appui sur la touche, et à cet instant là, le caractère n'est pas encore récupéré du clavier. Enfin, nous aurions pu pensé qu'il fallait plutôt prendre la méthode getTyped(), mais cette dernière ne délivre le caractère apparemment que sur la saisie du caractère suivant.

Choix du chapitre Retour sur les événements de la souris

Il n'est pas nécessaire de gérer explicitement les événements de la souris si vous désirez seulement que l'utilisateur puisse cliquer sur un bouton ou un menu. Ces opérations sont gérées de façon interne par les divers composants de l'interface utilisateur et traduites en événements sémantiques appropriés. Cependant, si vous voulez permettre à l'utilisateur de dessiner avec la souris, il vous faudra intercepter les mouvements, les clics et les opérations de glisser-déplacer de la souris.

Les événements générés par la souris appartiennent à la catégorie MouseEvent. Suivant le cas, ils sont gérés par un écouteur implémentant l'interface MouseListener et/ou l'interface MouseMotionListener.

Gestion des événements sans prise en compte du déplacement du curseur de la souris

Lorsque l'utilisateur clique sur un bouton de la souris, trois méthodes de l'écouteur MouseListener sont appelées :

  1. mousePressed() : quand le bouton est enfoncé,
  2. mouseReleased() : quand il est relâché,
  3. mouseCliked() : à la suite de la succession des deux événements précédents.

Vous pouvez ignorer les deux premières méthodes si vous n'êtes intéressé que par des clics complets.
.

En utilisant getX() et getY() sur l'argument MouseEvent, vous pouvez obtenir les coordonnées x et y du pointeur de la souris au moment du clic. Si vous souhaitez faire une distinction entre clic simple et double-clic (voir triple-clic), employez la méthode getClicCount().

Identification du bouton de la souris et les touches modificatrices du clavier

Il est possible de connaître le(s) bouton(s) de la souris concernée(s) pour un événement donné, en recourrant à la méthode getModifiers() de la class MouseEvent. Elle fournit un entier dans lequel le bit de rang donné est associé à chacun des boutons et prend la valeur 1 pour indentifier un appui. La classe InputEvent contient des constantes que nous pouvons utiliser comme masque afin de tester la présence d'un bouton donné dans la valeur issue de getModifiers().

Masque Bouton correspondant
InputEvent.BUTTON1_MASK gauche
InputEvent.BUTTON2_MASK central (s'il existe)
InputEvent.BUTTON3_MASK droite

Par exemple pour savoir si le bouton droit est enfoncé :

public void mousePressed(MouseEvent e) {
  if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) ...;
  ...
}
Lorsque nous nous interressons uniquement au relâchement du bouton droit, notamment pour déclencher l'affichage d'un menu surgissant, nous pouvons nous contenter d'utiliser la méthode isPopupTrigger() qui contrôle s'il s'agit bien du bon bouton (généralement le droit) pour activer le menu (dans ce cas, faites bien attention de réaliser le test dans mouseReleased() et non dans mouseClicked()).

Bien cela soit assez rarement souhaitable, vous pouvez combiner les boutons de la souris avec les touches modificatrices du clavier : Crtl, Shift, Alt, Alt Gr et Meta. Dans ce cas, prenez plutôt la méthode getModifiersEx() à la place de la méthode getModifiers(). La classe InputEvent dispose également des masques spécifiques à la gestion de ces touches modificatrices.

Masque Touche correspondante
InputEvent.SHIFT_DOWN_MASK majuscule (Shift)
InputEvent.CTRL_DOWN_MASK Ctrl
InputEvent.ALT_DOWN_MASK Alt
InputEvent.ALT_GRAPH_DOWN_MASK Alt Gr
InputEvent.META_DOWN_MASK Une des quatre touches précédentes

Par exemple pour savoir si le bouton droit est enfoncé en même temps de la touche Ctrl :

public void mousePressed(MouseEvent e) {
  if ((e.getModifiers() & (InputEvent.BUTTON3_MASK | InputEvent.CTRL_DOWN_MASK)) != 0) ...;
  ...
}

Gestion des déplacements de la souris

Dès que vous déplcez la souris, même sans cliquer sur un des boutons, vous provoquez des événements. Tous se passe comme si, à des intervalles de temps relativement réguliers, la souris signalait sa position, ce qui peut donner naissance à deux sortes d'événements :

Entrée-sortie de composant gérés par l'écouteur MouseListener
mouseEntered(MouseEvent) : généré chaque fois que la souris passe de l'extérieur à l'intérieur d'un composant.
mouseExited(MouseEvent) : généré chaque fois que la souris passe de l'intérieur à l'extérieur d'un composant.
Déplacement sur un composant géré par l'écouteur MouseMotionListener
mouseMoved(MouseEvent) : l'utilisateur déplace la souris sans tenir de bouton enfoncé.
mouseDragged(MouseEvent) : l'utilisateur déplace la souris en maintenant un bouton enfoncé. Notez bien que les événements mouseDragged continuent d'être générés (pour le composant concerné) même si la souris sort du composant, et ce jusqu'à ce que l'utilisateur relâche le bouton.

Nous devons encore expliquer comment écouter les événements de souris. Les clics sont signalés par la méthode mouseClicked(), qui fait partie de l'interface MouseListener. Comme de nombreuses applications ne s'intéressent qu'aux clics de souris - et pas à ces mouvements - et comme les événements de déplacement se produisent très fréquemment, les événements de glisser-déplacer de la souris sont définis dans une interface séparée appelée donc MouseMotionListener.

Exemple de gestion des mouvements de la souris

Afin d'illustrer toute cette approche, vous allez mettre en oeuvre un programme qui permet de déplacer un texte à l'aide de la souris. De plus, lorsque le curseur se déplace au dessus du texte, le message de bienvenue change de couleur, du bleu il passe au rouge. Nous pouvons remarquer que le texte est à la fois source des événements, mais également écouteur. Il devra donc s'occuper de la gestion de ses propres événements de la souris.

Pour les événements de la souris, nous venons de voir qu'il existe deux interfaces écouteurs spécifiques : une pour les mouvements de la souris - MouseMotionListener, l'autre pour tous les autres types d'événements - MouseListener. Une des solution consiste à fabriquer une classe Bienvenue qui hérite de JLabel et qui implémente ces deux interfaces. Elles permettent de prendre en compte le fait que le curseur de la souris passe au dessus du texte avec la méthode mouseEntered(), en sort avec la méthode mouseExited(), que l'on clique sur le texte pour mémoriser la position initiale à l'aide de la méthode mousePressed(), et enfin le déplacement de la souris avec le maintien de l'appui sur le bouton au moyen de la méthode mouseDragged() (Glisser-Déposer).






 

Choix du chapitre Les événements de focalisation

Lorsque vous utiliser une souris, vous pouvez pointer sur n'importe quel objet à l'écran. Mais, lorsque vous faites une saisie à l'aide du clavier, les frappes sur les touches doivent concerner un objet spécifique. Le gestionnaire de fenêtre (tel que Windows ou X Window) dirige toutes les frappes de touche vers la fenêtre active. Souvent la fenêtre active se distingue par une barre de titre en surbrillance. Une seule fenêtre peut être active à la fois.

La fenêtre Java reçoit à son tour les frappes du clavier et les dirige vers un composant particulier. Ce composant est désigné comme ayant le focus ou encore qu'il détient la focalisation. En effet, à un instant donné, seul un composant est actif, qui se traduit par une indication visuelle : un champ de texte contient une barre clignotante, un bouton comprend un rectangle autour du libellé, etc. Lorsqu'un champ de texte a le focus, vous pouvez ainsi taper du texte dedans. Lorsqu'un bouton a le focus, vous pouvez l'activer (effectuer la validation) en appuyant sur la barre d'espace du clavier.

Un seul composant dans une fenêtre peut avoir le focus à un instant donné. Un composant peut perdre le focus si l'utilisateur sélectionne un autre composant, qui obtient alors la focalisation.

Nous donnons le focus à un composant soit en cliquant dessus, soit en déplaçant l'indication visuel de focalisation à l'aide des touches Tab et Shift/Tab du clavier. Nous pouvons ainsi agir sur un composant ayant le focus à l'aide de la barre d'espace, ce qui équivaut à un clic. Par défaut, l'ordre de focalisation (ou ordre de tabulation) des composants Swing va de la gauche vers la droite et de haut en bas, selon la position dans leur conteneur. Cet ordre peut éventuellement être modifié.

Permettre la focalisation

Certains composants, comme les labels ou les panneaux n'obtiennent pas le focus par défaut car on suppose qu'ils ne sont utilisés que pour la décoration ou le groupage. Vous devez alors écraser ce paramètre par défaut si vous implémentez un programme de dessin avec des panneaux qui affichent des éléments en réponse au frappes du clavier. Il suffit alors de faire appel à la méthode setFocusable() :

panneau.setFocusable(true);

Connaître l'état du focus

Lorsqu'un composant possède le focus, il peut recevoir les événements clavier correspondants (si nous avons prévu un écouteur approprié). Ainsi, nous pouvons savoir si un composant donné possède le focus en appelant la méthode hashFocus() :

boolean test = composant.hasFocus();

Forcer le focus

Avec la programmation, vous pouvez déplacer le focus sur un autre composant en appelant la méthode requestFocus() de la classe Component :

composant.requestFocus();

Toutefois, le comportement dépend intrinsèquement de la plate-forme si le composant n'est pas dans la fenêtre ayant actuellement la focalisation. Pour permettre au programmeurs de développer un code indépendant de la plate-forme, nous pouvons utiliser alors la méthode requestFocusInWindow() de la classe Component. Cette méthode réussit uniquement si le composant est contenu dans la fenêtre ayant le focus.

Informations relatives à la focalisation du clavier

A partir du gestionnaire de focalisation du clavier, il est possible de connaître le composant qui possède le focus. Pour construire votre gestionnaire, voici ce que vous devez faire (pas besoin dans le cas d'un composant JFrame) :

KeyboardFocusManager gestionnaire = KeyboardFocusManager.getCurrentKeyboardFocusManager();

A partir du gestionnaire, nous pouvons alors facilement retrouver :

  1. Le propriétaire du focus : c'est-à-dire le composant qui a le focus :

    Component composant = gestionnaire.getFocusOwner();

  2. La fenêtre focalisée : la fenêtre qui contient le propriétaire du focus :

    Window focusFenêtre = gestionnaire.getFocusedWindow();

  3. La fenêtre active : le cadre ou la boîte de dialogue qui contient le propriétaire du focus :

    Window fenêtreActive = gestionnaire.getActiveWindow();

La fenêtre focalisée est généralement la même que la fenêtre active. Vous n'obtiendrez un résultat différent que lorsque le propriétaire du focus est contenu dans une fenêtre de haut niveau sans décoration de cadre, comme un menu contextuel.

Notification des changements de focalisation des composants graphiques usuels

Si vous désirez notifier les changements de focalisation, vous devez installer des écouteurs de focalisation dans les composants ou les fenêtres. Un écouteur de focalisation de composant doit implémenter l'interface FocusListener qui possède deux méthodes focusGained() et focusLost(). La prise du focus par un composant génère un événement de la catégorie FocusEvent que nous pouvons traiter par la méthode focusGained() de l'interface FocusListener. De la même manière, la perte du focus par un composant génère un événement du même type, que nous pouvons traiter, cette fois-ci, par la méthode focusLost().

Il existe plusieurs méthodes très utiles de la classe FocusEvent. Ainsi, la méthode getComponent() renvoie le composant qui a reçu ou perdu la focalisation. De même, grâce à la méthode isTemporary(), il est possible de savoir si une perte de focus est temporaire. Cette situation correspond au cas où un composant perd le focus, suite à un changement de fenêtre active : dans ce cas, en effet, le composant retrouvera automatiquement le focus quand l'utilisateur reviendra dans la fenêtre correspondante.

Notification des changements de focalisation des fenêtres

Il existe également un écouteur spécifique à la focalisation de fenêtre représenté par l'interface WindowFocusListener et qui possède les méthodes windowGainedFocus(WindowEvent) ainsi que windowLostFocus(WindowEvent) qui prennent respectivement en compte la prise ou la perte du focus de la fenêtre.

Vous pouvez retrouver le composant ou la fenêtre "opposée" au moment du transfert de focus. Lorsqu'un composant ou une fenêtre perd le focus, son opposé est le composant ou la fenêtre qui le récupère. A l'inverse, lorsqu'un composant ou une fenêtre prend le focus, son opposé est celui qui l'a perdu. La méthode getOppositeComponent() de la classe FocusEvent signale le composant opposé, et getOppositeWindow() de la classe WindowEvent signale la fenêtre opposée.

Exemple d'application

Nous allons, à titre d'exemple, mettre en oeuvre un jeu qui permet de déterminer un nombre qui a été choisi aléatoirement avec un nombre de coups limité. Ce jeu comporte trois phases qui seront implémentées par trois cartes différentes.

  1. La première phase : consiste à la configuration du jeu qui permet de choisir la valeur maximale limite du nombre aléatoire et du nombre de coups accepté.



  2. La deuxième phase : est le jeu proprement dit. L'utilisateur propose les nombres et un petit message d'avertissement indique si le nombre est plus grand ou plus petit.



  3. La dernière phase : indique le résultat, soit l'utilisateur a trouvé ou alors le programme indique le nombre à rechercher et affiche l'historique de l'ensemble des nombres proposés.

package jeu;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.event.*;

public class Aléatoire extends JFrame {   
   private int nombre, nombreAléatoire, tentative, maximum = 10, coup = 3;
   private ArrayList<Integer> historique = new ArrayList<Integer>();
   private boolean gagné = false;
   
   private Configuration configuration = new Configuration();
   private Jeu jeu = new Jeu();
   private Résultat résultat = new Résultat();
   private CardLayout pile = new CardLayout();
   
   public Aléatoire() {
      super("Nombre aléatoire");
      setLayout(pile);
      add(configuration, "configuration");
      add(jeu, "jeu");
      add(résultat, "résultat");      
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setResizable(false);
      setVisible(true);
   }
   
   public static void main(String[] args) { new  Aléatoire(); }
   
   private class Etiquette extends JLabel {
      public Etiquette(String intitulé) {
         super(intitulé);
         setHorizontalAlignment(RIGHT);
         setBorder(BorderFactory.createEtchedBorder());         
         setPreferredSize(new Dimension(112, 22));
      }
   }
   
   abstract private class Panneau extends JPanel implements ActionListener, FocusListener {
      protected JPanel panneau = new JPanel();
      protected JButton continuer = new JButton("Continuer");
      
      public Panneau() {
         setLayout(new BorderLayout());
         panneau.setBackground(Color.ORANGE);
         panneau.setLayout(new GridLayout(0, 2));
         add(panneau);
         add(continuer, BorderLayout.SOUTH);
         continuer.addActionListener(this);
         addFocusListener(this);
         setFocusable(true);
      }

      public void focusGained(FocusEvent e) { }
      public void focusLost(FocusEvent e) {}
   }
   
   private class Configuration extends Panneau implements ChangeListener, ActionListener {
      private JSpinner saisieMaximum = new JSpinner(new SpinnerNumberModel(10, 7, 100, 5));
      private JSpinner saisieCoup = new JSpinner(new SpinnerNumberModel(3, 3, 10, 1));
            
      public Configuration() {        
         panneau.add(new Etiquette("Maximum : "));
         panneau.add(saisieMaximum);
         panneau.add(new Etiquette("Coup : "));
         panneau.add(saisieCoup);        
         saisieMaximum.addChangeListener(this);        
         saisieCoup.addChangeListener(this);
      }

      @Override
      public void actionPerformed(ActionEvent e) {
         pile.next(getContentPane());
         jeu.requestFocus();
      }

      public void stateChanged(ChangeEvent e) {
         if (e.getSource() == saisieMaximum) maximum = (Integer)saisieMaximum.getValue();
         if (e.getSource() == saisieCoup) coup = (Integer)saisieCoup.getValue();
      }  
   }
   
   private class Jeu extends Panneau {
      private JTextField saisieNombre = new JTextField(nombre);
      private JLabel afficheRésultat = new JLabel("Tentez votre chance...");
      private JTextField lectureTentative = new JTextField("0");
      
      public Jeu() {
         panneau.add(new Etiquette("Valeur : "));
         panneau.add(saisieNombre);
         panneau.add(new Etiquette("Tentatives : "));
         lectureTentative.setEditable(false);
         panneau.add(lectureTentative);
         add(afficheRésultat, BorderLayout.SOUTH);
         saisieNombre.addActionListener(this);
      }

      @Override
      public void actionPerformed(ActionEvent e) {
         historique.add(nombre = Integer.parseInt(saisieNombre.getText()));
         tentative++;
         gagné = nombre == nombreAléatoire;
         if (gagné || tentative==coup) {
            pile.next(getContentPane());
            résultat.requestFocus();
         }      
         else {
            lectureTentative.setText(""+tentative);
            afficheRésultat.setText(" Le nombre est "+(nombreAléatoire>nombre ? "plus grand" : "plus petit"));
         }
      }
      
      @Override
      public void focusGained(FocusEvent e) {
         nombreAléatoire = (int)(Math.random()*maximum)+1;
         tentative = 0;
         afficheRésultat.setText("Tentez votre chance...");
         lectureTentative.setText("0");
         saisieNombre.setText("0");
         historique.clear();      
         pack();
      }
   }
   
   private class Résultat extends Panneau {
      @Override
      public void actionPerformed(ActionEvent e) {
         continuer.setText("Continuer");
         pile.next(getContentPane());
      }
      
      public void focusGained(FocusEvent e) {
         continuer.setText("Recommencer");
         panneau.removeAll();
         if (gagné) panneau.add(new JLabel(" Bravo ..."));
         else {
            panneau.add(new Etiquette("Nombre à trouver : "));
            panneau.add(new Etiquette(nombreAléatoire+" "));
            for (int i=0; i<historique.size(); i++) {
               panneau.add(new Etiquette("Coup n°"+(i+1)+" : "));
               panneau.add(new Etiquette(historique.get(i)+" "));
            } 
         } 
         pack();
      }
   }
}

 

Choix du chapitre Actions

Il existe souvent plusieurs manières d'activer une même commande. L'utilisateur peut effectivement choisir la fonction désirée, par exemple l'ouverture d'un fichier, soit par l'intermédiaire d'un menu, soit à partir d'un raccourci clavier ou soit en cliquant sur un bouton dans une barre d'outils.

Dans ce cadre là, si nous souhaitons réaliser des logiciels de qualité, il est préférable que le traitement de la commande (par exemple l'ouverture du fichier) ne soit réalisée qu'en un seul point du code. Nous pouvons déjà tendre vers cet idéal en faisant en sorte que les écouteurs appropriés se contentent d'appeler une méthode unique responsable de l'action en question. En général, cependant, cela ne sera pas suffisant et il faudra s'acheminer vers la création d'objets abstraits encapsulant toutes les informations nécessaires à la réalisation d'une action (par exemple la boîte de dialogue à faire apparaître, le nom de la méthode à solliciter, etc.).

C'est dans ce contexte que Java offre un outil très puissant. Il s'agit de la classe AbstractAction qui comporte déjà les services de base que nous pouvons attendre d'une classe destinée à représenter une telle action. Bien entendu, nous pourrons la compléter à volonté par héritage.

Retour sur les méthodes classiques du paquetage AWT

Avant de prendre connaissance de cette classe, revoyons le traitement de la gestion des événements de façon classique. Il est en effet très facile avec AWT de lier tous les événements au même écouteur. Par exemple, supposons que écouteurOuvrir soit un écouteur d'action dont la méthode actionPerformed() permet l'ouverture d'un fichier texte. Vous pouvez attacher le même objet comme écouteur de plusieurs sources d'événements :

  1. Un bouton de la barre d'outils libellé "Ouvrir" ;
  2. Une option du menu principal baptisée "Ouvrir" ;
  3. Une option du menu surgissant baptisée également "Ouvrir" ;
  4. Une combinaison de touches Crtl+O.

Puis chaque commande d'ouverture de fichier est gérée d'une seule manière, quelle que soit l'action qui l'a déclenchée : un clic sur un bouton, un choix sur un menu ou une frappe au clavier.

L'interface Action du paquetage Swing

Toutefois, le paquetage Swing fournit un mécanisme beaucoup plus pratique pour encapsuler des commandes et les attacher à plusieurs sources d'événement : il s'agit de l'interface Action. Une action est un objet qui encapsule :

  1. Une desciption de la commande (comme une chaîne de texte et une icône facultative) ;
  2. Les paramètres nécessaires à l'exécution de la commande (dans notre cas, l'ouverture d'un fichier).

L'interface Action possède les méthodes suivantes :

interface Action {
   void actionPerformed(ActionEvent événement);
   void setEnabled(boolean disponible);
   boolean isEnabled();
   void putValue(String clé, Object valeur);
   Object getValue(String clé);
   void addPropertyChangeListener(PropertyChangeListener écouteur);
   void removePropertyChangeListener(PorpertyChangeListener écouteur);
}

La première méthode actionPerformed() est la méthode habituelle de l'interface ActionListener : en fait, l'interface Action est dérivée de ActionListener. Par conséquent, il est possible d'utiliser un objet Action partout où un objet ActionListener est attendu.

Les deux méthodes suivantes setEnabled() et isEnabled() permettent d'activer ou de désactiver l'action, et de vérifier si elle est activée. Lorsqu'une action attachée à un menu ou à une barre d'outils est désactivée, l'option correspondante apparaît en grisé.


Les méthodes putValue() et getValue() sont employées pour stocker et récupérer un couple arbitraire clé/valeur dans l'objet Action. Il existe des chaînes prédéfinies comme Action.NAME et Action.SMALL_ICON pour faciliter le stockage des noms et des icônes dans un objet Action :
action.putValue(Action.NAME, "Ouvrir");
action.putValue(Action.SMALL_ICON, new ImageIcon("ouvrir.gif"));
Nom de l'action prédéfini Valeur de l'action
NAME Nom de l'action ; affiché sur les boutons et les options de menu.
SMALL_ICON Emplacement de stockage d'une petite icône ; pour affichage sur un bouton, une option de menu ou dans la barre d'outils.
SHORT_DESCRIPTION Courte description de l'icône ; pour affichage dans une bulle d'aide.
LONG_DESCRIPTION Description détaillée de l'icône ; pour utilisation potentielle dans l'aide en ligne. Aucun composant Swing n'utilise cette valeur.
MNEMONIC_KEY Abreviation mnémonique ; pour affichage dans une option de menu.
ACCELERATOR_KEY Emplacement pour le stockage d'un raccourci clavier. Aucun composant Swing n'utilise cette valeur.
ACTION_COMMAND_KEY Utilisée dans la méthode registerKeyboardAction(), maintenant obsolète.
DEFAULT Propiété fourre-tout. Aucun composant Swing n'utilise cette valeur.

Si l'objet Action est ajouté à un menu ou à une barre d'outils, le nom et l'icône sont automatiquement récupérés et affichés dans l'option du menu ou sur le bouton de la barre d'outils. La valeur de SHORT_DESCRIPTION s'affiche dans une bulle d'aide.

Les deux dernières méthodes addPropertyChangeListener() et removePropertyChangeListener() de l'interface Action permettent aux autres objets - en particulier les menus ou les barres d'outils qui ont déclenché l'action - de recevoir une notification lorsque les propriétés de l'objet Action sont modifiées. Par exemple, si un menu est recencé en tant qu'écouteur de changement de propriétés d'un objet Action, et que l'objet Action soit ensuite désactivé, le menu est prévenu et peut alors affiché en grisé la rubrique correspondant à l'action. Les écouteurs de changement de propriété sont une construction générique intégré au modèle de composant des JavaBeans.

L'objet AbstractAction implémente l'interface Action

Notez que Action est une interface et non une classe. Toute classe implémentant cette interface est donc tenue d'implémenter les sept méthodes citées. Heureusement, un bon samaritain a implémenté toutes ces méthodes - sauf actionPerformed() - dans une classe nommée AbstractAction, qui se charge de stocker les couples clé/valeur et de gérer les écouteurs de changement de propriété. Il ne vous reste plus qu'à étendre AbstractAction et à écrire la méthode actionPerformed().

Mise en oeuvre

Afin de bien illustrer l'intérêt de cette classe AbstractAction, je vous propose de mettre en oeuvre un simple éditeur de texte. Cet éditeur comporte un menu et des boutons qui proposent les mêmes actions, c'est-à-dire l'ouverture et la sauvegarde d'un texte. Chacun des éléments possède un libellé, une icône, une bulle d'aide avec, en plus, une activation possible par le clavier en proposant la combinaison des touches Alt+(première lettre de l'option). Le menu propose un item supplémentaire, qui n'existe pas sous forme de bouton, qui permet de créer un nouveau document. Pour finir, la rubrique "Enregistrer" peut apparaître en grisé si il n'y a eu aucune modification du texte dans l'éditeur.

Nous pourrions nous poser la question de l'intérêt de mettre en oeuvre la technique des actions lorsque nous disposons d'une seule source comme ici l'option du menu "Nouveau". Je pense personnellement que cette technique continue à être intéressante, puisque nous n'avons pas à placer séparément un libellé, une icône, une bulle d'aide, un raccourci clavier et à faire appel ensuite à la méthode addActionListener().

 1 package action;
 2 
 3 import java.awt.*;
 4 import java.awt.event.*;
 5 import java.io.*;
 6 import javax.swing.*;
 7 import javax.swing.event.*;
 8 
 9 public class Fenêtre extends JFrame {
10    private Actions actionNouveau = new Actions("Nouveau", "Tout effacer dans la zone d'édition");
11    private Actions actionOuvrir = new Actions("Ouvrir", "Ouvrir le fichier texte");
12    private Actions actionEnregistrer = new Actions("Enregistrer", "Sauvegarder le texte");
13    private JButton ouvrir = new JButton(actionOuvrir);
14    private JButton enregistrer = new JButton(actionEnregistrer);
15    private JMenuBar menu = new JMenuBar();
16    private JMenu fichier = new JMenu("Fichier");
17    private JPanel panneau = new JPanel();
18    private JTextPane éditeur = new JTextPane();
19    
20    public Fenêtre() {
21       super("Nouveau document");
22       setSize(350, 300);
23       setDefaultCloseOperation(EXIT_ON_CLOSE);
24       setJMenuBar(menu);
25       actionEnregistrer.setEnabled(false);
26       menu.add(fichier);
27       fichier.add(actionNouveau);
28       fichier.add(actionOuvrir);
29       fichier.add(actionEnregistrer);
30       panneau.add(ouvrir);
31       panneau.add(enregistrer);
32       éditeur.addKeyListener(new KeyAdapter() {
33          @Override
34          public void keyTyped(KeyEvent ev) {
35             actionEnregistrer.setEnabled(true);
36          }
37       });
38       add(new JScrollPane(éditeur));
39       add(panneau, BorderLayout.SOUTH);
40       setVisible(true);
41    }
42    
43    public static void main(String[] args) {
44        new Fenêtre();
45    }
46 
47    private class Actions extends AbstractAction {
48       private String méthode;
49       private JFileChooser boîte = new JFileChooser();
50       
51       public Actions(String libellé, String description) {
52          super(libellé, new ImageIcon(Fenêtre.class.getResource(libellé.toLowerCase()+".gif")));
53          putValue(SHORT_DESCRIPTION, description);
53          putValue(MNEMONIC_KEY, (int)libellé.charAt(0));
54          méthode = libellé.toLowerCase();
55       }
56       
57       public void actionPerformed(ActionEvent e) {
58          try {
59             this.getClass().getDeclaredMethod(méthode).invoke(this);
60          } 
61          catch (Exception ex) { Fenêtre.this.setTitle("Problème");}
62       }  
63       
64       private void nouveau() {
65          Fenêtre.this.setTitle("Nouveau document");
66          éditeur.setText("");
67          actionEnregistrer.setEnabled(false);
68       }
69       
70       private void ouvrir() throws IOException {
71          if (boîte.showOpenDialog(Fenêtre.this)==JFileChooser.APPROVE_OPTION) {
72             File fichier = boîte.getSelectedFile();
73             Fenêtre.this.setTitle(fichier.getName());
74             éditeur.read(new FileInputStream(fichier), null);
75          }         
76       }
77       
78       private void enregistrer() throws IOException {
79          if (boîte.showSaveDialog(Fenêtre.this)==JFileChooser.APPROVE_OPTION) {
80             File fichier = boîte.getSelectedFile();
81             Fenêtre.this.setTitle(fichier.getName());
82             if (!fichier.exists()) fichier.createNewFile();
83             PrintWriter écriture = new PrintWriter(fichier);
84             écriture.println(éditeur.getText());
85             écriture.close();
86          }           
87       }
88    }
89 }
Analyse du code
Lignes 47 à 88
Création d'une nouvelle action qui est une classe interne et qui hérite donc de la classe AbstractAction.
Lignes 48 et 49
Comme toute classe, il est tout à fait possible de proposer des attributs spécifiques. Le plus important est certainement le premier méthode qui enregistre le nom de la méthode qui sera systématiquement appelée lorsque l'événement de type ActionEvent sera sollicité.
Lignes 64 à 87
Description des méthodes relatif aux actions à réaliser suivant le cas, respectivement : nouveau(), ouvrir() et enregistrer().
Lignes 57 à 62
Méthode actionPerformed() à redéfinir qui s'occupe de l'action à réaliser lorsque l'événement se produit. Cette méthode fait ensuite appel indirectement (pointeur de méthode) à la méthode - nouveau(), ouvrir() ou enregistrer() - qui correspond au traitement souhaité (technique de la réflexion).
Lignes 10 à 12
Vous créez ensuite les actions correspondantes à l'apparence (libellé, icône, bulle d'aide) et au traitement désiré.
Lignes 13, 14 et 27 à 30
Vous associez ensuite ces actions aux composants qui les utilisent comme les boutons et les options du menu. Remarquez qu'il existe effectivement un constructeur de JButton qui accepte en argument une classe qui implémente Action, et donc toute classe qui hérite de AbstractButton. Remarquez de plus, qu'il suffit de faire appel à la simple méthode add() de JMenu pour intégrer une action qui sera ainsi automatiquement interprété comme un nouvel item avec tout ce qu'il faut : son libellé, son icône et sa bulle d'aide. Remarquez pour terminer, que vous ne faites jamais appel à une méthode addActionListener(), tout se fait automatiquement.
Lignes 25, 35 et 67
Gestion du grisé sur l'action représentant l'enregistrement.
Lignes 32 à 37
Prise en compte de l'événement relâchement d'une touche du clavier dans la zone d'édition permettant ainsi de contrôler si le contenu de l'éditeur a changé afin de permettre ensuite l'enregistrement du texte.

Choix du chapitre Créer des écouteurs contenant un seul appel de méthode

Java introduit un mécanisme séduisant qui vous permet de spécifier des écouteurs d'événement simples sans programmer de classes spécifiques (interne ou pas). Je vous propose de traiter ce sujet au travers de l'application de conversion où lorsque nous validons la valeur saisie en €uro, la transformation s'effectue automatiquement en Franc.

  1. Je rappelle toutefois la démarche classique qui consiste à créer une classe (interne ou pas) qui implémente l'interface ActionListener. Vous êtes alors dans l'obligation de redéfinir la ou les méthodes prévues par l'interface, ici actionPerformed() :

    Exemple d'application avec une conversion entre les €uros et les francs
    package format;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.*;
    import java.text.*;
    
    public class Conversion extends JFrame implements ActionListener {
       private JFormattedTextField saisie = new JFormattedTextField(NumberFormat.getCurrencyInstance());
       private JFormattedTextField résultat = new JFormattedTextField(new DecimalFormat("#,##0.00 F"));
       
       public Conversion() {
          super("Conversion €uro -> Francs");
          saisie.setColumns(25);
          saisie.setValue(0);
          add(saisie, BorderLayout.NORTH);
          résultat.setEditable(false);
          résultat.setValue(0);
          add(résultat, BorderLayout.SOUTH);
          saisie.addActionListener(this);
          pack();
          setResizable(false);
          setDefaultCloseOperation(EXIT_ON_CLOSE);
          setVisible(true);
       }
    
       public void actionPerformed(ActionEvent e) {
          final double TAUX = 6.55957;
          double €uro = ((Number)saisie.getValue()).doubleValue();
          double franc = €uro * TAUX;
          résultat.setValue(franc);
       }
       
       public static void main(String[] args)  { new  Conversion(); }   
    }
    
  2. Or la classe EventHandler peut créer automatiquement cet écouteur, grâce à l'intervention de sa méthode create() :
    EventHandler.create(ActionListener.class, this, "actionPerformed");
     // vous pouvez choisir votre propre nom de méthode (par exemple "calcul")  et ne pas utiliser celui imposer par l'interface
    Vous devrez malgré tout installer le gestionnaire :
    saisie.addActionListener(EventHandler.create(ActionListener.class, this, "calcul"));   

    La classe EventHandler
    static Object create(Class écouteur, Object cible, String action)
    static Object create(Class écouteur, Object cible, String action, String propriétés de l'événement)
    Ces méthodes construisent un objet d'une classe proxy qui implémente l'interface donnée. La méthode nommée (action) ou toutes les méthodes de l'interface réalisent l'action donnée sur l'objet cible.

    L'action peut être un nom de méthode ou une propriété. S'il s'agit d'une propriété, sa méthode "setter" est exécutée. Par exemple, une action "text" est transformée en appel de la méthode setText() de l'objet cible.

    La propriété d'événement est constituée d'un ou de plusieurs noms de propriété séparés par un point. La première propriété est lue depuis le paramètre de la méthode écouteur. La deuxième est lue à partir de l'objet de résultat, etc. Le résultat définitif devient le paramètre de l'action. Par exemple, la propriété "source.text" est transformée en appels respectifs des méthodes getSource() et getText() correspondant au paramètre de l'écouteur, comme ActionEvent.

    EventHandler.create(ActionListener.class, fenêtre, "loadData", "source.text");      
    équivaut à :
       public void actionPerformed(ActionEvent e) {
          fenêtre.loadData((JTextField) e.getSource().getText());
       }     
  3. Reprenons l'application de conversion et utilisons à la place la classe EventHandler :

    Exemple d'application avec une conversion entre les €uros et les francs
    package format;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.*;
    import java.text.*;
    
    public class Conversion extends JFrame {
       private JFormattedTextField saisie = new JFormattedTextField(NumberFormat.getCurrencyInstance());
       private JFormattedTextField résultat = new JFormattedTextField(new DecimalFormat("#,##0.00 F"));
       
       public Conversion() {
          super("Conversion €uro -> Francs");
          saisie.setColumns(25);
          saisie.setValue(0);
          add(saisie, BorderLayout.NORTH);
          résultat.setEditable(false);
          résultat.setValue(0);
          add(résultat, BorderLayout.SOUTH);
          saisie.addActionListener(EventHandler.create(ActionListener.class, this, "calcul"));
          pack();
          setResizable(false);
          setDefaultCloseOperation(EXIT_ON_CLOSE);
          setVisible(true);
       }
    
       public void calcul() {
          final double TAUX = 6.55957;
          double €uro = ((Number)saisie.getValue()).doubleValue();
          double franc = €uro * TAUX;
          résultat.setValue(franc);
       }
       
       public static void main(String[] args)  { new  Conversion(); }   
    }
        

 

Choix du chapitre Et plus encore

Il existe bien d'autres événements qui sont plus liés aux composants de la bibliothèque Swing. Ils sont associés à des composants qui propose une gestion événementielle bien spécifique. Je vous propose donc un certain nombre de liens qui indique les écouteurs à prendre en compte et qui vous renvoie vers les composants concernés :

  1. DocumentListener : Changement dans le texte d'un document inclus dans un JTextComponent qui est l'ancêtre d'un JTextField, JTextArea, etc.
  2. CaretListener : Changement de la position du curseur de texte dans un de ces composants.
  3. HyperlinkListener : Liens hypertexte dans un composant JEditorPane ou JTextPane.
  4. ChangeListener, ItemListener et AdjustmentListener : Evénements associées aux composants de choix.