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.
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 :
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);
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.
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.
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 :
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.
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 :
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".
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
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).
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().
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 :
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 :
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.
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) ?
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.
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().
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().
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.
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.
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.
.
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.
AWT fait une distinction utile entre événements de bas niveau et événements sémantiques :
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.
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()
Les interfaces suivantes permettent d'écouter ces événements :
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.
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 :
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.
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.
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.
.
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.
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 :
Si vous désirez que votre minuteur fonctionne, pensez bien à le démarrer au moyen de la méthode start().
.
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())); } }
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.
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.
.
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.
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).
.
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 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 :
Par exemple, la frappe du caractère A entraînera les appels suivants :
En revanche, la frappe du caractère a (minuscule) n'entraîne que les appels suivants :
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().
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é.
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.
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 :
Java considère qu'un événement clavier possède comme sources :
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.
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()); } }
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.
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.
Lorsque l'utilisateur clique sur un bouton de la souris, trois méthodes de l'écouteur MouseListener sont appelées :
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().
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 |
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 |
public void mousePressed(MouseEvent e) { if ((e.getModifiers() & (InputEvent.BUTTON3_MASK | InputEvent.CTRL_DOWN_MASK)) != 0) ...; ... }
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 :
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.
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).
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é.
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);
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();
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.
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 :
Component composant = gestionnaire.getFocusOwner();
Window focusFenêtre = gestionnaire.getFocusedWindow();
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.
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.
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.
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.
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(); } } }
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.
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 :
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.
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 :
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é.
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.
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().
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 }
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.
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(); } }
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
saisie.addActionListener(EventHandler.create(ActionListener.class, this, "calcul"));
EventHandler.create(ActionListener.class, fenêtre, "loadData", "source.text");équivaut à :
public void actionPerformed(ActionEvent e) { fenêtre.loadData((JTextField) e.getSource().getText()); }
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(); } }
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 :