IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Penser en Java

2nde édition


précédentsommairesuivant

XV. Création de fenêtres et d'applets

Une directive de conception fondamentale est « rendre les choses simples faciles, et les choses difficiles possibles ».

L'objectif initial de la conception de la bibliothèque d'interface utilisateur graphique [graphical user interface (GUI)] en Java 1.0 était de permettre au programmeur de construire une GUI qui a un aspect agréable sur toutes les plateformes. Ce but n'a pas été atteint. L'Abstract Window Toolkit (AWT) de Java 1.0 produit au contraire une GUI d'aspect aussi médiocre sur tous les systèmes. De plus, elle est restrictive : on ne peut utiliser que quatre fontes, et on n'a accès à aucun des éléments de GUI sophistiqués disponibles dans son système d'exploitation.Le modèle de programmation de l'AWT Java 1.0 est aussi maladroit et non orienté objet. Un étudiant d'un de mes séminaires (qui était chez Sun lors de la création de Java) m'a expliqué pourquoi : l'AWT original avait été imaginé, conçu, et implémenté en un mois. Certainement une merveille de productivité, mais aussi un exemple du fait que la conception est importante.

La situation s'est améliorée avec le modèle d'événements de l'AWT de Java 1.1, qui a une approche beaucoup plus claire et orientée objet, avec également l'ajout des JavaBeans, un modèle de programmation par composants qui est tourné vers la création facile d'environnements de programmation visuels. Java 2 termine la transformation depuis l'ancien AWT de Java 1.0 en remplaçant à peu près tout par les Java Foundation Classes (JFC), dont la partie GUI est appelée « Swing ». Il s'agit d'un ensemble riche de JavaBeans faciles à utiliser et faciles à comprendre, qui peuvent être glissés et déposés (ou programmés manuellement) pour créer une GUI qui vous donnera (enfin) satisfaction. La règle de la « version 3 » de l'industrie du logiciel (un produit n'est bon qu'à partir de la version 3) semble également se vérifier pour les langages de programmation.

Ce chapitre ne couvre que la bibliothèque moderne Swing de Java 2, et fait l'hypothèse raisonnable que Swing est la bibliothèque finale pour les GUI Java. Si pour une quelconque raison vous devez utiliser le « vieux » AWT d'origine (parce que vous maintenez du code ancien ou que vous avez des limitations dans votre browser), vous pouvez trouver cette présentation dans la première édition de ce livre, téléchargeable à www.BruceEckel.com (également incluse sur le CD-ROM fourni avec ce livre).

Dès le début de ce chapitre, vous verrez combien les choses sont différentes selon que vous voulez créer une applet ou une application normale utilisant Swing, et comment créer des programmes qui sont à la fois des applets et des applications, de sorte qu'ils puissent être exécutés soit dans un browser soit depuis la ligne de commande. Presque tous les exemples de GUI seront exécutables aussi bien comme des applets que comme des applications.

Soyez conscients que ceci n'est pas un glossaire complet de tous les composants Swing, ou de toutes les méthodes pour les classes décrites. Ce qui se trouve ici a pour but d'être simple. La bibliothèque Swing est vaste, et le but de ce chapitre est uniquement de vous permettre de démarrer avec les notions essentielles et d'être à l'aise avec les concepts. Si vous avez besoin de plus, alors Swing peut probablement vous donner ce que vous voulez si vous avez la volonté de faire des recherches.

Je suppose ici que vous avez téléchargé et installé les documents (gratuits) de la bibliothèque Java au format HTML depuis java.sun.com et que vous parcourrez les classes javax.swing dans cette documentation pour voir tous les détails et toutes les méthodes de la bibliothèque Swing. Grâce à la simplicité de la conception de Swing, ceci sera souvent suffisant pour régler votre problème. Il y a de nombreux livres (assez épais) dédiés uniquement à Swing et vous vous y référerez si vous avez besoin d'approfondir, ou si vous voulez modifier le comportement par défaut de Swing.

En apprenant Swing vous découvrirez que :

  1. Swing est un bien meilleur modèle de programmation que ce que vous avez probablement vu dans d'autres langages et environnements de développement. Les JavaBeans (qui seront présentées vers la fin de ce chapitre) constituent l'ossature de cette bibliothèque ;
  2. Les « GUI builders »(environnements de programmation visuels) sont un must-have d'un environnement de développement Java complet. Les JavaBeans et Swing permettent au « builder » d'écrire le code pour vous lorsque vous placez les composants sur des formulaires à l'aide d'outils graphiques. Ceci permet non seulement d'accélérer le développement lors de la création de GUI, mais aussi d'expérimenter plus et de ce fait d'essayer plus de modèles et probablement d'en obtenir un meilleur ;
  3. La simplicité et la bonne conception de Swing signifie que même si vous utilisez un GUI builder plutôt que du codage manuel, le code résultant sera encore compréhensible ; ceci résout un gros problème qu'avaient les GUI builders, qui généraient souvent du code illisible.

Swing contient tous les composants attendus dans une interface utilisateur moderne, depuis des boutons contenant des images jusqu'à des arborescences et des tables. C'est une grosse bibliothèque, mais elle est conçue pour avoir la complexité appropriée pour la tâche à effectuer ; si quelque chose est simple, vous n'avez pas besoin d'écrire beaucoup de code, mais si vous essayez de faire des choses plus compliquées, votre code devient proportionnellement plus complexe. Ceci signifie qu'il y a un point d'entrée facile, mais que vous avez la puissance à votre disposition si vous en avez besoin.

Ce que vous aimerez dans Swing est ce qu'on pourrait appeler l'« orthogonalité d'utilisation ». C'est-à-dire, une fois que vous avez compris les idées générales de la bibliothèque, vous pouvez les appliquer partout. Principalement grâce aux conventions de nommage standards, en écrivant ces exemples je pouvais la plupart du temps deviner le nom des méthodes et trouver juste du premier coup, sans rechercher quoi que ce soit. C'est certainement la marque d'une bonne conception de la bibliothèque. De plus, on peut en général connecter des composants entre eux et les choses fonctionnent correctement.

Pour des questions de rapidité, tous les composants sont « légers » , et Swing est écrit entièrement en Java pour la portabilité.

La navigation au clavier est automatique ; vous pouvez exécuter une application Swing sans utiliser la souris, et ceci ne réclame aucune programmation supplémentaire. Le scrolling se fait sans effort, vous emballez simplement votre composant dans un JScrollPane lorsque vous l'ajoutez à votre formulaire. Des fonctionnalités telles que les « infobulles » [tool tips] ne demandent qu'une seule ligne de code pour les utiliser.

Swing possède également une fonctionnalité assez avancée, appelée le « pluggable look and feel », qui signifie que l'apparence de l'interface utilisateur peut être modifiée dynamiquement pour s'adapter aux habitudes des utilisateurs travaillant sur des plateformes et systèmes d'exploitation différents. Il est même possible (quoique difficile) d'inventer son propre « look and feel ».

XV-A. L'applet de base

Un des buts de la conception Java est de créer des applets, qui sont des petits programmes s'exécutant à l'intérieur d'un browser Web. Parce qu'elles doivent être sûres, les applets sont limitées dans ce qu'elles peuvent accomplir. Toutefois, les applets sont un outil puissant de programmation côté client, un problème majeur pour le Web.

XV-A-1. Les restrictions des applets

Programmer dans une applet est tellement restrictif qu'on en parle souvent comme étant « dans le bac à sable », car il y a toujours quelqu'un (le système de sécurité Java lors de l'exécution [Java run-time security system] ) qui vous surveille.

Cependant, on peut aussi sortir du bac à sable et écrire des applications normales plutôt que des applets ; dans ce cas on accède aux autres fonctionnalités de son OS. Nous avons écrit des applications tout au long de ce livre, mais il s'agissait d'applications console, sans aucun composant graphique. Swing peut aussi être utilisé pour construire des interfaces utilisateur graphiques pour des applications normales.

On peut en général répondre à la question de savoir ce qu'une applet peut faire en considérant ce qu'elle est supposée faire : étendre les fonctionnalités d'une page Web dans un browser. Comme en tant que surfeur sur le Net on ne sait jamais si une page Web vient d'un site amical ou pas, on veut que tout le code qu'il exécute soit sûr. De ce fait, les restrictions les plus importantes sont probablement :

  1. Une applet ne peut pas toucher au disque local. Ceci signifie écrire ou lire, puisqu'on ne voudrait pas qu'une applet lise et transmette des informations privées sur Internet sans notre permission. Écrire est interdit, bien sûr, car ce serait une invitation ouverte aux virus. Java permet une signature digitale des applets. Beaucoup de restrictions des applets sont levées si on autorise des applets de confiance [trusted applets] (celles qui sont signées par une source dans laquelle on a confiance) à accéder à sa machine ;
  2. Les applets peuvent mettre du temps à s'afficher, car il faut les télécharger complètement à chaque fois, nécessitant un accès au serveur pour chacune des classes. Le browser peut mettre l'applet dans son cache, mais ce n'est pas garanti. Pour cette raison, on devrait toujours empaqueter ses applets dans un fichier JAR (Java ARchive) qui rassemble tous les composants de l'applet (y compris d'autres fichiers .class ainsi que les images et les sons) en un seul fichier compressé qui peut être téléchargé en une seule transaction avec le serveur. La « signature digitale » existe pour chacun des fichiers contenus dans le fichier JAR.

Les avantages d'une applet

Si on admet ces restrictions, les applets ont des avantages, en particulier pour la création d'applications client/serveur ou réseaux :

  1. Il n'y a pas de problème d'installation. Une applet est réellement indépendante de la plateforme (y compris pour jouer des fichiers audio, etc.) et donc il n'est pas nécessaire de modifier le code en fonction des plateformes ni d'effectuer des « réglages » à l'installation. En fait, l'installation est automatique chaque fois qu'un utilisateur charge la page Web qui contient les applets, de sorte que les mises à jour se passent en silence et automatiquement. Dans les systèmes client/serveur traditionnels, créer et installer une nouvelle version du logiciel client est souvent un cauchemar ;
  2. On ne doit pas craindre un code défectueux affectant le système de l'utilisateur, grâce au système de sécurité inclus au cœur du langage Java et de la structure de l'applet. Ceci, ainsi que le point précédent, rend Java populaire pour les applications intranet client/serveur qui n'existent qu'à l'intérieur d'une société ou dans une zone d'opérations réduite, où l'environnement utilisateur (le navigateur Web et ses extensions) peut être spécifié et/ou contrôlé.

Comme les applets s'intègrent automatiquement dans le code HTML, on dispose d'un système de documentation intégré et indépendant de la plateforme pour le support de l'applet. C'est une astuce intéressante, car on a plutôt l'habitude d'avoir la partie documentation du programme plutôt que l'inverse.

XV-A-3. Les squelettes d'applications

Les bibliothèques sont souvent groupées selon leurs fonctionnalités. Certaines bibliothèques, par exemple, sont utilisées telles quelles. Les classes String et ArrayList de la bibliothèque standard Java en sont des exemples. D'autres bibliothèques sont conçues comme des briques de construction pour créer d'autres classes. Une certaine catégorie de bibliothèques est le squelette d'applications [application framework], dont le but est d'aider à la construction d'applications en fournissant une classe ou un ensemble de classes reproduisant le comportement de base dont vous avez besoin dans chaque application d'un type donné. Ensuite, pour particulariser le comportement pour ses besoins propres, on hérite de la classe et on redéfinit les méthodes qui nous intéressent. Un squelette d'application est un bon exemple de la règle « séparer ce qui change de ce qui ne change pas », car on essaie de localiser les parties spécifiques d'un programme dans les méthodes redéfinies (49)

les applets sont construites à l'aide d'un squelette d'application. On hérite de la classe JApplet et on redéfinit les méthodes adéquates. Quelques méthodes contrôlent la création et l'exécution d'une applet dans une page Web :

Method Operation
init() Appelée automatiquement pour effectuer la première initialisation de l'applet, y compris la disposition des composants. Il faut toujours redéfinir cette méthode.
start() Appelée chaque fois que l'applet est rendue visible du navigateur Web, pour permettre à l'applet de démarrer ses opérations normales (en particulier celles qui sont arrêtées par stop()). Appelée également après init().
stop() Appelée chaque fois que l'applet redevient invisible du navigateur Web, pour permettre à l'applet d'arrêter les opérations coûteuses. Appelée également juste avant destroy().
destroy() Appelée lorsque l'applet est déchargée de la page pour effectuer la libération finale des ressources lorsque l'applet n'est plus utilisée.

Avec ces informations nous sommes prêts à créer une applet simple :

 
Sélectionnez
//: c13:Applet1.java
// Très simple applet.
import javax.swing.*;
import java.awt.*;

public class Applet1 extends JApplet {
  public void init() {
  getContentPane().add(new JLabel("Applet!"));
}
} ///:~

Remarquons qu'il n'est pas obligatoire qu'une applet ait une méthode main(). C'est complètement câblé dans le squelette d'application ; on met tout le code de démarrage dans init().

Dans ce programme, la seule activité consiste à mettre un label de texte sur l'applet, à l'aide de la classe JLabel (l'ancien AWT s'était approprié le nom Label ainsi que quelques autres noms de composants, de sorte qu'on verra souvent un « J » au début des composants Swing). Le constructeur de cette classe reçoit une String et l'utilise pour créer le label. Dans le programme ci-dessus ce label est placé dans le formulaire.

La méthode init() est responsable du placement de tous les composants du formulaire en utilisant la méthode add(). On pourrait penser qu'il devrait être suffisant d'appeler simplement add(), et c'est ainsi que cela se passait dans l'ancien AWT. Swing exige quant à lui qu'on ajoute tous les composants sur la « vitre de contenu » [content pane] d'un formulaire, et il faut donc appeler getContentPane() au cours de l'opération add().

XV-A-4. Exécuter des applets dans un navigateur Web

Pour exécuter ce programme, il faut le placer dans une page Web et visualiser cette page à l'aide d'un navigateur Web configuré pour exécuter des applets Java. Pour placer une applet dans une page Web, on place un tag spécial dans le source HTML de cette page (50) pour dire à la page comment charger et exécuter cette applet.

Ce système était très simple lorsque Java lui-même était simple et que tout le monde était dans le même bain et incorporait le même support de Java dans les navigateurs Web. Il suffisait alors d'un petit bout de code HTML dans une page Web, tel que :

 
Sélectionnez
<applet code=Applet1 width=100 height=50>
</applet>

Ensuite vint la guerre des navigateurs et des langages, et nous (programmeurs aussi bien qu'utilisateurs finals) y avons perdu. Au bout d'un moment, JavaSoft s'est rendu compte qu'on ne pouvait plus supposer que les navigateurs supportaient la version correcte de Java, et que la seule solution était de fournir une sorte d'extension qui se conformerait au mécanisme d'extension des navigateurs. En utilisant ce mécanisme d'extensions (qu'un fournisseur de navigateur ne peut pas supprimer - dans l'espoir d'y gagner un avantage sur la concurrence - sans casser aussi toutes les autres extensions), JavaSoft garantit que Java ne pourra pas être supprimé d'un navigateur Web par un fournisseur qui y serait hostile.

Avec Internet Explorer, le mécanisme d'extensions est le contrôle ActiveX, et avec Netscape c'est le plug-in. Dans le code HTML, il faut fournir les tags qui supportent les deux. Voici à quoi ressemble la page HTML la plus simple pour Applet1 : (51)

 
Sélectionnez
//:! c13:Applet1.html
<html><head><title>Applet1</title></head><hr>
<OBJECT 
classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
width="100" height="50" align="baseline"  codebase="http://java.sun.com/products/plugin/1.2.2/jinstall-1_2_2-win.cab#Version=1,2,2,0">
<PARAM NAME="code" VALUE="Applet1.class">
<PARAM NAME="codebase" VALUE=".">
<PARAM NAME="type" VALUE="application/x-java-applet;version=1.2.2">
<COMMENT>
<EMBED type=  "application/x-java-applet;version=1.2.2" 
  width="200" height="200" align="baseline"
  code="Applet1.class" codebase="."
pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html">
<NOEMBED>
</COMMENT>
No Java 2 support for APPLET!!
</NOEMBED>
</EMBED>
</OBJECT>
<hr></body></html>
///:~

Certaines de ces lignes étaient trop longues et ont été coupées pour être insérées dans cette page. Le code inclus dans les sources de ce livre (sur le CD-ROM de ce livre, et téléchargeable sur www.BruceEckel.com) fonctionne sans qu'on doive s'occuper de corriger les sauts de lignes.

Le contenu de code fournit le nom du fichier .class dans lequel se trouve l'applet. Les paramètres width et height spécifient la taille initiale de l'applet (en pixels, comme avant). Il y a d'autres éléments qu'on peut placer dans le tag applet : un endroit où on peut trouver d'autres fichiers .class sur Internet (codebase), des informations d'alignement (align), un identificateur spécial permettant à des applets de communiquer entre eux (name), et des paramètres des applets pour fournir des informations que l'applet peut récupérer. Les paramètres sont de la forme :

 
Sélectionnez
<param name="identifier" value = "information">

et il peut y en avoir autant qu'on veut.

Le package de codes source de ce livre fournit une page HTML pour chacune des applets de ce livre, et de ce fait de nombreux exemples du tag applet. Vous pouvez trouver une description complète et à jour des détails concernant le placement d'applets dans des pages web à l'adresse java.sun.com.

XV-A-5. Utilisation de Appletviewer

Le JDK de Sun (en téléchargement libre depuis java.sun.com) contient un outil appelé l'Appletviewer qui extrait le tag du fichier HTML et exécute les applets sans afficher le texte HTML autour. Comme l'Appletviewer ignore tout ce qui n'est pas tags APPLET, on peut mettre ces tags dans le source Java en commentaires :

 
Sélectionnez
// <applet code=MyApplet width=200 height=100>
// </applet>

De cette manière, on peut lancer « appletviewer MyApplet.java » et il n'est pas nécessaire de créer de petits fichiers HTML pour lancer des tests. Par exemple, on peut ajouter ces tags HTML en commentaires dans Applet1.java :

 
Sélectionnez
//: c13:Applet1b.java
// Embedding the applet tag for Appletviewer.
// <applet code=Applet1b width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;

public class Applet1b extends JApplet {>
  public void >init() {
  getContentPane().add(new JLabel("Applet!"));
}
} ///:~

Maintenant on peut invoquer l'applet avec la commande

 
Sélectionnez
appletviewer Applet1b.java

XV-A-6. Tester les applets

On peut exécuter un test simple sans aucune connexion réseau en lançant son navigateur Web et en ouvrant le fichier HTML contenant le tag applet. Au chargement du fichier HTML, le navigateur découvre le tag applet et part à la recherche du fichier .class spécifié par le contenu de code. Bien sûr, il utilise le CLASSPATH pour savoir où chercher, et si le fichier .class n'est pas dans le CLASSPATH, il émettra un message d'erreur dans sa ligne de statut pour signaler qu'il n'a pas pu trouver le fichier .class.

Quand on veut essayer ceci sur son site Web les choses sont un peu plus compliquées. Tout d'abord il faut avoir un site Web, ce qui pour la plupart des gens signifie avoir un Fournisseur d'Accès à Internet (FAI) [Internet Service Provider (ISP)]. Comme l'applet est simplement un fichier ou un ensemble de fichiers, le FAI n'a pas besoin de fournir un support particulier pour Java. Il faut disposer d'un moyen pour copier les fichiers HTML et les fichiers .class depuis chez vous vers le bon répertoire sur la machine du FAI. Ceci est normalement fait avec un programme de File Transfer Protocol (FTP), dont il existe beaucoup d'exemples disponibles gratuitement ou comme sharewares. Il semblerait donc que tout ce qu'il y a à faire est d'envoyer les fichiers sur la machine du FAI à l'aide de FTP, et ensuite de se connecter au site et au fichier HTML en utilisant son navigateur ; si l'applet se charge et fonctionne, alors tout va bien, n'est-ce pas ?

C'est là qu'on peut se faire avoir. Si le navigateur de la machine client ne peut pas localiser le fichier .class sur le serveur, il va le rechercher à l'aide du CLASSPATH sur la machine locale. De ce fait l'applet pourrait bien ne pas se charger correctement depuis le serveur, mais tout paraît correct lors du test parce que le navigateur le trouve sur la machine locale. Cependant, lorsque quelqu'un d'autre se connecte, son navigateur ne la trouvera pas. Donc lorsque vous testez, assurez-vous d'effacer les fichiers .class (ou .jar) de votre machine locale pour vérifier qu'ils existent au bon endroit sur le serveur.

Un des cas les plus insidieux qui m'est arrivé s'est produit lorsque j'ai innocemment placé une applet dans un package. Après avoir téléchargé sur le serveur le fichier HTML et l'applet, le serveur fut trompé sur le chemin d'accès à l'applet à cause du nom du package. Cependant, mon navigateur l'avait trouvé dans le CLASSPATH local. J'étais donc le seul à pouvoir charger correctement l'applet. J'ai mis un certain temps à découvrir que l'instruction package était la coupable. En général il vaut mieux ne pas utiliser l'instruction package dans une applet.

XV-B. Exécuter des applets depuis la ligne de commande

Parfois on voudrait qu'un programme fenêtré fasse autre chose que se trouver dans une page Web. Peut-être voudrait-on aussi faire certaines des choses qu'une application « normale » peut faire, mais en gardant la glorieuse portabilité instantanée fournie par Java. Dans les chapitres précédents de ce livre, nous avons fait des applications de ligne de commande, mais dans certains environnements (le Macintosh par exemple) il n'y a pas de ligne de commande. Voilà un certain nombre de raisons pour vouloir construire un programme fenêtré n'étant pas une applet. C'est certainement un désir légitime.

La bibliothèque Swing nous permet de construire une application qui conserve le « look and feel » du système d'exploitation sous-jacent. Si vous voulez faire des applications fenêtrées, cela n'a de sens (52) que si vous pouvez utiliser la dernière version de Java et ses outils associés, afin de pouvoir fournir des applications qui ne perturberont pas vos utilisateurs. Si pour une raison ou une autre vous devez utiliser une ancienne version de Java, réfléchissez-y bien avant de vous lancer dans la construction d'une application fenêtrée importante.

On a souvent besoin de créer une classe qui peut être appelée aussi bien comme une fenêtre que comme une applet. C'est particulièrement utile lorsqu'on teste des applets, car il est souvent plus facile et plus simple de lancer l'applet-application depuis la ligne de commande que de lancer un navigateur Web ou l'Appletviewer.

Pour créer une applet qui peut être exécutée depuis la ligne de commande, il suffit d'ajouter un main() à l'applet, dans lequel on construit une instance de l'applet dans un JFrame (53). En tant qu'exemple simple, observons Applet1b.java modifié pour fonctionner aussi bien en tant qu'application qu'en tant qu'applet :

 
Sélectionnez
//: c13:Applet1c.java
// Une application et une applet.
// <applet code=Applet1c width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Applet1c extends JApplet {
  public void init() {
  getContentPane().add(new JLabel("Applet!"));
}
  // Un main() pour l'application :
  public static void main(String[] args) {
  JApplet applet = new Applet1c();
  JFrame frame = new JFrame("Applet1c");
   // Pour fermer l'application :
  Console.setupClosing(frame);
  frame.getContentPane().add(applet);
  frame.setSize(100,50);
  applet.init();
  applet.start();
  frame.setVisible(true);
}
} ///:~

main() est le seul élément ajouté à l'applet, et le reste de l'applet n'est pas modifié. L'applet est créée et ajoutée à un JFrame pour pouvoir être affichée.

La ligne :

 
Sélectionnez
Console.setupClosing(frame);

permet la fermeture propre de la fenêtre. Console vient de com.bruceeckel.swing et sera expliqué un peu plus tard.

On peut voir que dans main(), l'applet est explicitement initialisée et démarrée, car dans ce cas le navigateur n'est pas là pour le faire. Bien sûr, ceci ne fournit pas le comportement complet du navigateur, qui appelle aussi stop() et destroy(), mais dans la plupart des cas c'est acceptable. Si cela pose un problème, on peut forcer les appels soi-même href="#fn67">(54).

Notez la dernière ligne :

 
Sélectionnez
frame.setVisible(true);

Sans elle, on ne verrait rien à l'écran.

XV-B-1. Un squelette d'affichage

Bien que le code qui transforme des programmes en applets et applications produise des résultats corrects, il devient perturbant et gaspille du papier s'il est utilisé partout. Au lieu de cela, le squelette d'affichage ci-après sera utilisé pour les exemples Swing du reste de ce livre :

 
Sélectionnez
//: com:bruceeckel:swing:Console.java
// Outil pour exécuter des démos Swing depuis
// la console, aussi bien applets que JFrames.
package com.bruceeckel.swing;
import javax.swing.*;
import java.awt.event.*;

public class Console {
  // Crée une chaîne de titre à partir du nom de la classe :
  public static String title(Object o) {
  String t = o.getClass().toString();
   // Enlever le mot "class":
   if(t.indexOf("class") != -1)
    t = t.substring(6);
   return t;
}
  public static void setupClosing(JFrame frame) {
   // La solution JDK 1.2 Solution avec une 
   // classe interne anonyme :
  frame.addWindowListener(new WindowAdapter() {
    public void windowClosing(WindowEvent e) {
      System.exit(0);
    }
  });
   // La solution améliorée en JDK 1.3 :
   // frame.setDefaultCloseOperation(
   //     EXIT_ON_CLOSE);
}
  public static void 
run(JFrame frame, int width, int height) {
  setupClosing(frame);
  frame.setSize(width, height);
  frame.setVisible(true);
}
  public static void 
run(JApplet applet, int width, int height) {
  JFrame frame = new JFrame(title(applet));
  setupClosing(frame);
  frame.getContentPane().add(applet);
  frame.setSize(width, height);
  applet.init();
  applet.start();
  frame.setVisible(true);
}
  public static void 
run(JPanel panel, int width, int height) {
  JFrame frame = new >JFrame(title(panel));
  setupClosing(frame);
  frame.getContentPane().add(panel);
  frame.setSize(width, height);
  frame.setVisible(true);
}
} ///:~

Comme c'est un outil que vous pouvez utiliser vous-mêmes, il est placé dans la bibliothèque com.bruceeckel.swing. La classe Console contient uniquement des méthodes static. La première est utilisée pour extraire le nom de la classe (en utilisant RTTI) depuis n'importe quel objet, et pour enlever le mot « class », qui est ajouté normalement au début du nom par getClass(). On utilise les méthodes de String : indexOf() pour déterminer si le mot « class » est présent, et substring() pour générer la nouvelle chaîne sans « class » ou le blanc de fin. Ce nom est utilisé pour étiqueter la fenêtre qui est affichée par les méthodes run().

setupClosing() est utilisé pour cacher le code qui provoque la sortie du programme lorsque la JFrame est fermée. Le comportement par défaut est de ne rien faire, donc si on n'appelle pas setupClosing() ou un code équivalent pour le JFrame, l'application ne se ferme pas. Une des raisons pour lesquelles ce code est caché plutôt que d'être placé directement dans les méthodes run() est que cela nous permet d'utiliser la méthode en elle-même lorsque ce qu'on veut faire est plus complexe que ce que fournit run(). Mais il isole aussi un facteur de changement : Java 2 possède deux manières de provoquer la fermeture de certains types de fenêtres. En JDK 1.2, la solution est de créer une nouvelle classe WindowAdapter et d'implémenter windowClosing(), comme vu plus haut (la signification de ceci sera expliquée en détail plus tard dans ce chapitre). Cependant lors de la création du JDK 1.3, les concepteurs de la bibliothèque ont observé qu'on a normalement besoin de fermer des fenêtres chaque fois qu'on crée un programme qui n'est pas une applet, et ils ont ajouté setDefaultCloseOperation() à JFrame et JDialog. Du point de vue de l'écriture du code, la nouvelle méthode est plus agréable à utiliser, mais ce livre a été écrit alors qu'il n'y avait pas de JDK 1.3 implémenté sur Linux et d'autres plateformes, et donc dans l'intérêt de la portabilité toutes versions, la modification a été isolée dans setupClosing().

La méthode run() est surchargée pour fonctionner avec les JApplets, les JPanels, et les JFrames. Remarquez que init() et start() ne sont appelées que s'il s'agit d'une JApplet.

Maintenant toute applet peut être lancée de la console en créant un main() contenant une ligne comme celle-ci :

 
Sélectionnez
Console.run(new MyClass(), 500, 300);

dans laquelle les deux derniers arguments sont la largeur et la hauteur de l'affichage. Voici Applet1c.java modifié pour utiliser Console :

 
Sélectionnez
//: c13:Applet1d.java
// Console exécute des applets depuis la ligne de commande.
// <applet code=Applet1d width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Applet1d extends JApplet {
  public void init() {
  getContentPane().add(new JLabel("Applet!"));
}
  public static void main(String[] args) {
  Console.run(new Applet1d(), 100, 50);
}
} ///:~

Ceci permet l'élimination de code répétitif tout en fournissant la plus grande flexibilité pour lancer les exemples.

XV-B-2. Utilisation de l'Explorateur Windows

Si vous utilisez Windows, vous pouvez simplifier le lancement d'un programme Java en ligne de commande en configurant l'explorateur Windows (le navigateur de fichiers de Windows, pas Internet Explorer) de façon à pouvoir double-cliquer sur un fichier .class pour l'exécuter. Il y a plusieurs étapes à effectuer.

D'abord, téléchargez et installez le langage de programmation Perl depuis www.Perl.org. Vous trouverez sur ce site les instructions et la documentation sur ce langage.

Ensuite, créez le script suivant sans la première et la dernière ligne (ce script fait partie du package de sources de ce livre) :

 
Sélectionnez
//:! c13:RunJava.bat
@rem = '--*-Perl-*--
@echo off
perl -x -S "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9
goto endofperl
@rem ';
#!perl
$file = $ARGV[0];
$file =~ s/(.*)\..*/\1/;
$file =~ s/(.*\\)*(.*)/$+/;
&#180;java $file&#180;;
__END__
:endofperl
///:~

Maintenant, ouvrez l'explorateur Windows, sélectionnez Affichage, Options des dossiers, et cliquez sur l'onglet "Types de fichiers". Cliquez sur le bouton "Nouveau type...". Comme "Description du type", entrez "fichier classe Java". Comme "Extension associée", entrez class. Sous "Actions", cliquez sur le bouton "Nouveau...". Comme "Action" entrez "open", et pour "Application utilisée pour effectuer l'action" entrez une ligne telle que celle-ci :

 
Sélectionnez
"c:\aaa\Perl\RunJava.bat" "%L"

en personnalisant le chemin devant RunJava.bat en fonction de l'endroit où vous avez placé le fichier batch.

Une fois cette installation effectuée, vous pouvez exécuter tout programme Java simplement en double-cliquant sur le fichier .class qui contient un main().

XV-C. Création d'un bouton

La création d'un bouton est assez simple : il suffit d'appeler le constructeur JButton avec le label désiré sur le bouton. On verra plus tard qu'on peut faire des choses encore plus jolies, comme y mettre des images graphiques.

En général on créera une variable pour le bouton dans la classe courante, afin de pouvoir s'y référer plus tard.

Le JButton est un composant possédant sa propre petite fenêtre qui sera automatiquement repeinte lors d'une mise à jour. Ceci signifie qu'on ne peint pas explicitement un bouton ni d'ailleurs les autres types de contrôles ; on les place simplement sur le formulaire et on les laisse se repeindre automatiquement. Le placement d'un bouton sur un formulaire se fait dans init() :

 
Sélectionnez
//: c13:Button1.java
// Placement de boutons sur une applet.
// <applet code=Button1 width=200 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Button1 extends JApplet {
JButton 
b1 = new JButton("Button 1"), 
b2 = new JButton("Button 2");
  public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(b1);
cp.add(b2);
}
  public static void main(String[] args) {
Console.run(new Button1(), 200, 50);
}
} ///:~

On a ajouté ici quelque chose de nouveau : avant d'ajouter un quelconque élément sur la "surface de contenu"[content pane], on lui attribue un nouveau gestionnaire de disposition [layout manager], de type FlowLayout. Le layout manager définit la façon dont la surface décide implicitement de l'emplacement du contrôle dans le formulaire. Le comportement d'une applet est d'utiliser le BorderLayout, mais cela ne marchera pas ici, car (comme on l'apprendra plus tard dans ce chapitre lorsqu'on verra avec plus de détails le contrôle de l'organisation d'un formulaire) son comportement par défaut est de couvrir entièrement chaque contrôle par tout nouveau contrôle ajouté. Cependant, FlowLayout provoque l'alignement des contrôles uniformément dans le formulaire, de gauche à droite et de haut en bas.

XV-D. Capture d'un événement

Vous remarquerez que si vous compilez et exécutez l'applet ci-dessus, rien ne se passe lorsqu'on appuie sur le bouton. C'est à vous de jouer et d'écrire le code qui définira ce qui va se passer. La base de la programmation par événements, qui est très importante dans les interfaces utilisateurs graphiques, est de lier les événements au code qui répond à ces événements.

Ceci est effectué dans Swing par une séparation claire de l'interface (les composants graphiques) et l'implémentation (le code que vous voulez exécuter quand un événement arrive sur un composant). Chaque composant Swing peut répercuter tous les événements qui peuvent lui arriver, et il peut répercuter chaque type d'événement individuellement. Donc si par exemple on n'est pas intéressé par le fait que la souris est déplacée par-dessus le bouton, on n'enregistre pas son intérêt pour cet événement. C'est une façon très directe et élégante de gérer la programmation par événements, et une fois qu'on a compris les concepts de base on peut facilement utiliser les composants Swing qu'on n'a jamais vus auparavant. En fait, ce modèle s'étend à tout ce qui peut être classé comme un JavaBean (que nous verrons plus tard dans ce chapitre).

Au début, on s'intéressera uniquement à l'événement le plus important pour les composants utilisés. Dans le cas d'un JButton, l'événement intéressant est le fait qu'on appuie sur le bouton. Pour enregistrer son intérêt à l'appui sur un bouton, on appelle la méthode addActionListener() de JButton. Cette méthode attend un argument qui est un objet qui implémente l'interface ActionListener, qui contient une seule méthode appelée actionPerformed(). Donc tout ce qu'il faut faire pour attacher du code à un JButton est d'implémenter l'interface ActionListener dans une classe et d'enregistrer un objet de cette classe avec le JButton à l'aide de addActionListener(). La méthode sera appelée lorsque le bouton sera enfoncé (ceci est en général appelé un callback).

Mais que doit être le résultat de l'appui sur ce bouton ? On aimerait voir quelque chose changer à l'écran ; pour cela on va introduire un nouveau composant Swing : le JTextField. C'est un endroit où du texte peut être tapé, ou dans notre cas modifié par le programme. Bien qu'il y ait plusieurs façons de créer un JTextField, la plus simple est d'indiquer au constructeur uniquement quelle largeur on désire pour ce champ. Une fois le JTextField placé sur le formulaire, on peut modifier son contenu en utilisant la méthode setText() (il y a beaucoup d'autres méthodes dans JTextField, que vous pouvez découvrir dans la documentation HTML pour le JDK depuis java.sun.com). Voilà à quoi ça ressemble :

 
Sélectionnez
//: c13:Button2.java
// Réponse aux appuis sur un bouton.
// <applet code=Button2 width=200 height=75>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Button2 extends JApplet {
JButton 
  b1 = new JButton("Button 1"), 
  b2 = new JButton("Button 2");
JTextField txt = new JTextField(10);
  class BL implements ActionListener {
   public void actionPerformed(ActionEvent e){
    String name = 
      ((JButton)e.getSource()).getText();
    txt.setText(name);
  }
}
BL al = new BL();
  public void init() {
  b1.addActionListener(al);
  b2.addActionListener(al);
  Container cp = getContentPane();
  cp.setLayout(new FlowLayout());
  cp.add(b1);
  cp.add(b2);
  cp.add(txt);
}
  public static void main(String[] args) {
  Console.run(new Button2(), 200, 75);
}
} ///:~

Dans init(), addActionListener() est utilisée pour enregistrer l'objet BL pour chacun des boutons.

Il est souvent plus pratique de coder l'ActionListener comme une classe anonyme interne [anonymous inner class], particulièrement lorsqu'on a tendance à n'utiliser qu'une seule instance de chaque classe listener. Button2.java peut être modifié de la façon suivante pour utiliser une classe interne anonyme :

 
Sélectionnez
//: c13:Button2b.java
// Utilisation de classes anonymes internes.
// <applet code=Button2b width=200 height=75>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Button2b extends JApplet {
JButton 
  b1 = new JButton("Button 1"), 
  b2 = new JButton("Button 2");
JTextField txt = new JTextField(10);
ActionListener al = new ActionListener() {
   public void actionPerformed(ActionEvent e){
    String name = 
      ((JButton)e.getSource()).getText();
    txt.setText(name);
  }
};
  public void init() {
  b1.addActionListener(al);
  b2.addActionListener(al);
  Container cp = getContentPane();
  cp.setLayout(new FlowLayout());
  cp.add(b1);
  cp.add(b2);
  cp.add(txt);
}
  public static void main(String[] args) {
  Console.run(new Button2b(), 200, 75);
}
} ///:~

L'utilisation d'une classe anonyme interne sera préférée (si possible) pour les exemples de ce livre.

XV-E. Zones de texte

Un JTextArea est comme un JTextField, sauf qu'il peut avoir plusieurs lignes et possède plus de fonctionnalités. Une méthode particulièrement utile est append() ; avec cette méthode on peut facilement transférer une sortie dans un JTextArea, faisant de ce fait d'un programme Swing une amélioration (du fait qu'on peut scroller en arrière) par rapport à ce qui a été fait jusqu'ici en utilisant des programmes de ligne de commande qui impriment sur la sortie standard. Comme exemple, le programme suivant remplit un JTextArea avec la sortie du générateur geography du titre XI :

 
Sélectionnez
//: c13:TextArea.java
// Utilisation du contrôle JTextArea.
// <applet code=TextArea width=475 height=425>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
import com.bruceeckel.swing.*;
import com.bruceeckel.util.*;

public class TextArea extends JApplet {
JButton 
  b = new JButton("Add Data"),
  c = new JButton("Clear Data");
JTextArea t = new JTextArea(20, 40);
Map m = new HashMap();
  public void init() {
   // Utilisation de toutes les données :
  Collections2.fill(m, 
    Collections2.geography, 
    CountryCapitals.pairs.length);
  b.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e){
      for(Iterator it= m.entrySet().iterator();
          it.hasNext();){
        Map.Entry me = (Map.Entry)(it.next());
        t.append(me.getKey() + ": " 
          + me.getValue() + "\n");
      }
    }
  });
  c.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e){
      t.setText("");
    }
  });
  Container cp = getContentPane();
  cp.setLayonew FlowLayout());
  cp.add(new JScrollPane(t));
  cp.add(b);
  cp.add(c);
}
  public static void main(String[] args) {
  Console.run(new TextArea(), 475, 425);
}
} ///:~

Dans init(), le Map est rempli avec tous les pays et leurs capitales. Remarquons que pour chaque bouton l'ActionListener est créé et ajouté sans définir de variable intermédiaire, puisqu'on n'aura plus jamais besoin de s'y référer dans la suite du programme. Le bouton "Add Data" formate et ajoute à la fin toutes les données, tandis que le bouton "Clear Data" utilise setText() pour supprimer tout le texte du JTextArea.

Lors de l'ajout du JTextArea à l'applet, il est enveloppé dans un JScrollPane, pour contrôler le scrolling quand trop de texte est placé à l'écran. C'est tout ce qu'il y a à faire pour fournir des fonctionnalités de scrolling complètes. Ayant essayé d'imaginer comment faire l'équivalent dans d'autres environnements de programmation de GUI, je suis très impressionné par la simplicité et la bonne conception de composants tels que le JScrollPane.

XV-F. Contrôle de la disposition

La façon dont on place les composants sur un formulaire en Java est probablement différente de tout système de GUI que vous avez utilisé. Premièrement, tout est dans le code ; il n'y a pas de ressources qui contrôlent le placement des composants. Deuxièmement, la façon dont les composants sont placés dans un formulaire est contrôlée non pas par un positionnement absolu, mais par un layout manager qui décide comment les composants sont placés, selon l'ordre dans lequel on les ajoute ( add() ). La taille, la forme et le placement des composants seront notablement différents d'un layout manager à l'autre. De plus, les gestionnaires de disposition s'adaptent aux dimensions de l'applet ou de la fenêtre de l'application, de sorte que si la dimension de la fenêtre est changée, la taille, la forme et le placement des composants sont modifiés en conséquence.

JApplet, JFrame, JWindow et JDialog peuvent chacun fournir un Container avec getContentPane() qui peut contenir et afficher des Components. Dans Container, il y a une méthode appelée setLayout() qui permet de choisir le layout manager. D'autres classes telles que JPanel contiennent et affichent des composants directement, et donc il faut leur imposer directement le layout manager, sans utiliser le content pane.

Dans cette section nous allons explorer les divers gestionnaires de disposition en créant des boutons (puisque c'est ce qu'il y a de plus simple). Il n'y aura aucune capture d'événements de boutons puisque ces exemples ont pour seul but de montrer comment les boutons sont disposés.

XV-F-1. BorderLayout

L'applet utilise un layout manager par défaut : le BorderLayout (certains des exemples précédents ont modifié le layout manager par défaut pour FlowLayout). Sans autre information, il prend tout ce qu'on lui ajoute ( add() ) et le place au centre, en étirant l'objet dans toutes les directions jusqu'aux bords.

Cependant le BorderLayout ne se résume pas qu'à cela. Ce layout manager possède le concept d'une zone centrale et de quatre régions périphériques. Quand on ajoute quelque chose à un panel qui utilise un BorderLayout, on peut utiliser la méthode add() surchargée qui prend une valeur constante comme premier argument. Cette valeur peut être une des suivantes :

BorderLayout.NORTH (en haut) BorderLayout.SOUTH (en bas) BorderLayout.EAST (à droite) BorderLayout.WEST (à gauche) BorderLayout.CENTER (remplir le milieu, jusqu'aux autres composants ou jusqu'aux bords)

Si aucune région n'est spécifiée pour placer l'objet, le défaut est CENTER.

Voici un exemple simple. Le layout par défaut est utilisé, puisque JApplet a BorderLayout par défaut :

 
Sélectionnez
//: c13:BorderLayout1.java
// Démonstration de BorderLayout.
// <applet code=BorderLayout1 
// width=300 height=250> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class BorderLayout1 extends JApplet {
  public void init() {
  Container cp = getContentPane();
  cp.add(BorderLayout.NORTH, 
    new JButton("North"));
  cp.add(BorderLayout.SOUTH, 
    new JButton("South"));
  cp.add(BorderLayout.EAST, 
    new JButton("East"));
  cp.add(BorderLayout.WEST, 
    new JButton("West"));
cp.add(BorderLayout.CENTER, 
    new JButton("Center"));
}
  public static void main(String[] args) {
  Console.run(new BorderLayout1(), 300, 250);
}
} ///:~

Pour chaque placement autre que CENTER, l'élément qu'on ajoute est comprimé pour tenir dans le plus petit espace le long d'une dimension et étiré au maximum le long de l'autre dimension. CENTER, par contre, s'étend dans chaque dimension pour occuper le milieu.

XV-F-2. FlowLayout

Celui-ci aligne simplement les composants sur le formulaire, de gauche à droite jusqu'à ce que l'espace du haut soit rempli, puis descend d'une rangée et continue l'alignement.

Voici un exemple qui positionne le layout manager en FlowLayout et place ensuite des boutons sur le formulaire. On remarquera qu'avec FlowLayout les composants prennent leur taille naturelle. Un JButton, par exemple, aura la taille de sa chaîne.

 
Sélectionnez
//: c13:FlowLayout1.java
// Démonstration de FlowLayout.
// <applet code=FlowLayout1 
// width=300 height=250> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class FlowLayout1 extends JApplet {
  public void init() {
  Container cp = getContentPane();
  cp.setLayout(new FlowLayout());
   for(int i = 0; i     cp.add(new JButton("Button " + i));
}
  public static void main(String[] args) {
  Console.run(new FlowLayout1(), 300, 250);
}
} ///:~

Tous les composants sont compactés à leur taille minimum dans un FlowLayout, ce qui fait qu'on peut parfois obtenir un comportement surprenant. Par exemple, vu qu'un JLabel prend la taille de sa chaîne, une tentative de justifier à droite son texte ne donne pas de modification de l'affichage dans un FlowLayout.

XV-F-3. GridLayout

Un GridLayout permet de construire un tableau de composants, et lorsqu'on les ajoute ils sont placés de gauche à droite et de haut en bas dans la grille. Dans le constructeur on spécifie le nombre de rangées et de colonnes nécessaires, et celles-ci sont disposées en proportions identiques.

 
Sélectionnez
//: c13:GridLayout1.java
// Démonstration de GridLayout.
// <applet code=GridLayout1 
// width=300 height=250> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class GridLayout1 extends JApplet {
  public void init() {
  Container cp = getContentPane();
  cp.setLayout(new GridLayout(7,3));
   for(int i = 0; i     cp.add(new JButton("Button " + i));
}
  public static void main(String[] args) {
  Console.run(new GridLayout1(), 300, 250);
}
} ///:~

Dans ce cas il y a 21 cases, mais seulement 20 boutons. La dernière case est laissée vide, car il n'y a pas d'équilibrage dans un GridLayout.

XV-F-4. GridBagLayout

Le GridBagLayout nous donne un contrôle fin pour décider exactement comment les régions d'une fenêtre vont se positionner et se replacer lorsque la fenêtre est redimensionnée. Cependant, c'est aussi le plus compliqué des layout managers, et il est assez difficile à comprendre. Il est destiné principalement à la génération de code automatique par un constructeur d'interfaces utilisateurs graphiques [GUI builder] (les bons GUI builders utilisent GridBagLayout plutôt que le placement absolu). Si votre modèle est compliqué au point que vous sentiez le besoin d'utiliser le GridBagLayout, vous devrez dans ce cas utiliser un outil GUI builder pour générer ce modèle. Si vous pensez devoir en connaître les détails internes, je vous renvoie à Core Java 2 par Horstmann & Cornell (Prentice-Hall, 1999), ou un livre dédié à Swing, comme point de départ.

XV-F-5. Positionnement absolu

Il est également possible de forcer la position absolue des composants graphiques de la façon suivante :

  1. Positionner un layout manager null pour le Container : setLayout(null) ;
  2. Appeler setBounds() ou reshape() (selon la version du langage) pour chaque composant, en passant un rectangle de limites avec ses coordonnées en pixels. Ceci peut se faire dans le constructeur, ou dans paint(), selon le but désiré.

Certains GUI builders utilisent cette approche de manière extensive, mais ce n'est en général pas la meilleure manière de générer du code. Les GUI builders les plus efficaces utilisent plutôt GridBagLayout.

XV-F-6. BoxLayout

Les gens ayant tellement de problèmes pour comprendre et utiliser GridBagLayout, Swing contient également le BoxLayout, qui offre la plupart des avantages du GridBagLayout sans en avoir la complexité, de sorte qu'on peut souvent l'utiliser lorsqu'on doit coder à la main des layouts (encore une fois, si votre modèle devient trop compliqué, utilisez un GUI builder qui générera les GridBagLayouts à votre place). BoxLayout permet le contrôle du placement des composants soit verticalement soit horizontalement, et le contrôle de l'espace entre les composants en utilisant des choses appelées struts (entretoises) et glue (colle). D'abord, voyons comment utiliser BoxLayout directement, en faisant le même genre de démonstration que pour les autres layout managers :

 
Sélectionnez
//: c13:BoxLayout1.java
// BoxLayouts vertical et horizontal.
// <applet code=BoxLayout1 
// width=450 height=200> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class BoxLayout1 extends JApplet {
  public void init() {
  JPanel jpv = new JPanel();
  jpv.setLayout(
    new BoxLayout(jpv, BoxLayout.Y_AXIS));
   for(int i = 0; i     jpv.add(new JButton("" + i));
  JPanel jph = new JPanel();
  jph.setLayout(
    new BoxLayout(jph, BoxLayout.X_AXIS));
   for(int i = 0; i     jph.add(new JButton("" + i));
  Container cp = getContentPane();
  cp.add(BorderLayout.EAST, jpv);
  cp.add(BorderLayout.SOUTH, jph);
}
  public static void main(String[] args) {
  Console.run(new BoxLayout1(), 450, 200);
}
} ///:~

Le constructeur du BoxLayout est un peu différent des autres layout managers : on fournit le Container que le BoxLayout doit contrôler comme premier argument, et la direction du layout comme deuxième argument.

Pour simplifier les choses, il y a un container spécial appelé Box qui utilise BoxLayout comme manager d'origine. L'exemple suivant place les composants horizontalement et verticalement en utilisant Box, qui possède deux méthodes static pour créer des boxes avec des alignements verticaux et horizontaux :

 
Sélectionnez
//: c13:Box1.java
// BoxLayouts vertical et horizontal.
// <applet code=Box1 
// width=450 height=200> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Box1 extends JApplet {
  public void init() {
  Box bv = Box.createVerticalBox();
   for(int i = 0; i     bv.add(new JButton("" + i));
  Box bh = Box.createHorizontalBox();
   for(int i = 0; i     bh.add(new JButton("" + i));
  Container cp = getContentPane();
  cp.add(BorderLayout.EAST, bv);
  cp.add(BorderLayout.SOUTH, bh);
}
  public static void main(String[] args) {
  Console.run(new Box1(), 450, 200);
}
} ///:~

Une fois qu'on a obtenu un Box, on le passe en second argument quand on ajoute des composants au content pane.

Les struts ajoutent de l'espace entre les composants, mesuré en pixels. Pour utiliser un strut, on l'ajoute simplement entre les ajouts de composants que l'on veut séparer :

 
Sélectionnez
//: c13:Box2.java
// Ajout de struts.
// <applet code=Box2 
// width=450 height=300> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Box2 extends JApplet {
  public void init() {
  Box bv = Box.createVerticalBox();
   for(int i = 0; i     bv.add(new JButton("" + i));
    bv.add(Box.createVerticalStrut(i*10));
  }
  Box bh = Box.createHorizontalBox();
   for(int i = 0; i     bh.add(new JButton("" + i));
    bh.add(Box.createHorizontalStrut(i*10));
  }
  Container cp = getContentPane();
  cp.add(BorderLayout.EAST, bv);
  cp.add(BorderLayout.SOUTH, bh);
}
  public static void main(String[] args) {
  Console.run(new Box2(), 450, 300);
}
} ///:~

//: c13:Box3.java
// Utilisation de Glue.
// <applet code=Box3 
// width=450 height=300> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Box3 extends JApplet {
  public void init() {
  Box bv = Box.createVerticalBox();
  bv.add(new JLabel("Hello"));
  bv.add(Box.createVerticalGlue());
  bv.add(new JLabel("Applet"));
  bv.add(Box.createVerticalGlue());
  bv.add(new JLabel("World"));
  Box bh = Box.createHorizontalBox();
  bh.add(new JLabel("Hello"));
  bh.add(Box.createHorizontalGlue());
  bh.add(new JLabel("Applet"));
  bh.add(Box.createHorizontalGlue());
  bh.add(new JLabel("World"));
  bv.add(Box.createVerticalGlue());
  bv.add(bh);
  bv.add(Box.createVerticalGlue());
  getContentPane().add(bv);
}
  public static void main(String[] args) {
  Console.run(new Box3(), 450, 300);
}
} ///:~

Un strut fonctionne dans une direction, mais une rigid area (surface rigide) fixe l'espace entre les composants dans chaque direction :

 
Sélectionnez
//: c13:Box4.java
// Des Rigid Areas sont comme des paires de struts.
// <applet code=Box4 
// width=450 height=300> </applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Box4 extends JApplet {
  public void init() {
  Box bv = Box.createVerticalBox();
  bv.add(new JButton("Top"));
  bv.add(Box.createRigidArea(
    new Dimension(120, 90)));
  bv.add(new JButton("Bottom"));
  Box bh = Box.createHorizontalBox();
  bh.add(new JButton("Left"));
  bh.add(Box.createRigidArea(
    new Dimension(160, 80)));
  bh.add(new JButton("Right"));
  bv.add(bh);
  getContentPane().add(bv);
}
  public static void main(String[] args) {
  Console.run(new Box4(), 450, 300);
}
} ///:~

Il faut savoir que les rigid areas sont un peu controversées. Comme elles utilisent des valeurs absolues, certaines personnes pensent qu'elles causent plus de problèmes qu'elles n'en résolvent.

XV-F-7. La meilleure approche ?

Swing est puissant ; il peut faire beaucoup de choses en quelques lignes de code. Les exemples de ce livre sont raisonnablement simples, et dans un but d'apprentissage il est normal de les écrire à la main. On peut en fait réaliser pas mal de choses en combinant des layouts simples. À un moment donné, il devient cependant déraisonnable de coder à la main des formulaires de GUI. Cela devient trop compliqué et ce n'est pas une bonne manière d'utiliser son temps de programmation. Les concepteurs de Java et de Swing ont orienté le langage et ses bibliothèques de manière à soutenir des outils de construction de GUI, qui ont été créés dans le but de rendre la tâche de programmation plus facile. À partir du moment où on comprend ce qui se passe dans les layouts et comment traiter les événements (décrits plus loin), il n'est pas particulièrement important de connaître effectivement tous les détails sur la façon de positionner les composants à la main. Laissons les outils appropriés le faire pour nous (Java est après tout conçu pour augmenter la productivité du programmeur).

XV-G. Le modèle d'événements de Swing

Dans le modèle d'événements de Swing, un composant peut initier (envoyer [fire]) un événement. Chaque type d'événement est représenté par une classe distincte. Lorsqu'un événement est envoyé, il est reçu par un ou plusieurs écouteurs [listeners], qui réagissent à cet événement. De ce fait, la source d'un événement et l'endroit où cet événement est traité peuvent être séparés. Puisqu'on utilise en général les composants Swing tels quels, mais qu'il faut écrire du code appelé lorsque les composants reçoivent un événement, ceci est un excellent exemple de la séparation de l'interface et de l'implémentation.

Chaque écouteur d'événements [event listener] est un objet d'une classe qui implémente une interface particulière de type listener. En tant que programmeur, il faut créer un objet listener et l'enregistrer avec le composant qui envoie l'événement. Cet enregistrement se fait par appel à une méthode addXXXListener() du composant envoyant l'événement, dans lequel XXX représente le type d'événement qu'on écoute. On peut facilement savoir quels types d'événements peuvent être gérés en notant les noms des méthodes addListener, et si on essaie d'écouter des événements erronés, l'erreur sera signalée à la compilation. On verra plus loin dans ce chapitre que les JavaBeans utilisent aussi les noms des méthodes addListener pour déterminer quels événements un Bean peut gérer.

Toute notre logique des événements va se trouver dans une classe listener. Lorsqu'on crée une classe listener, la seule restriction est qu'elle doit implémenter l'interface appropriée. On peut créer une classe listener globale, mais on est ici dans un cas où les classes internes sont assez utiles, non seulement parce qu'elles fournissent un groupement logique de nos classes listener à l'intérieur des classes d'interface utilisateur ou de logique métier qu'elles desservent, mais aussi (comme on le verra plus tard) parce que le fait qu'un objet d'une classe interne garde une référence à son objet parent fournit une façon élégante d'appeler à travers les frontières des classes et des sous-systèmes.

Jusqu'ici, tous les exemples de ce chapitre ont utilisé le modèle d'événements Swing, mais le reste de cette section va préciser les détails de ce modèle.

XV-G-1. Événements et types de listeners

Chaque composant Swing contient des méthodes addXXXListener() et removeXXXListener() de manière à ce que les types de listeners adéquats puissent être ajoutés et enlevés de chaque composant. On remarquera que le XXX dans chaque cas représente également l'argument de cette méthode, par exemple : addMyListener(MyListener m). Le tableau suivant contient les événements, listeners et méthodes de base associées aux composants de base qui supportent ces événements particuliers en fournissant les méthodes addXXXListener() et removeXXXListener(). Il faut garder en tête que le modèle d'événements est destiné à être extensible, et donc on pourra rencontrer d'autres types d'événements et de listeners non couverts par ce tableau.

Événement, interface listener et méthodes add et remove Composants supportant cet événement
ActionEvent ActionListener addActionListener() removeActionListener() JButton, JList, JTextField, JMenuItem et ses dérivés, comprenant JCheckBoxMenuItem, JMenu, et JpopupMenu.
AdjustmentEvent AdjustmentListener addAdjustmentListener() removeAdjustmentListener() JScrollbar et tout ce qu'on crée qui implémente l'interface Adjustable.
ComponentEvent ComponentListener addComponentListener() removeComponentListener() *Component et ses dérivés, comprenant JButton, JCanvas, JCheckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea, et JTextField.
ContainerEvent ContainerListener addContainerListener() removeContainerListener() Container et ses dérivés, comprenant JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, et JFrame.
FocusEvent FocusListener addFocusListener() removeFocusListener() Component et dérivés*.
KeyEvent KeyListener addKeyListener() removeKeyListener() Component et dérivés*.
MouseEvent (à la fois pour les clics et pour le déplacement) MouseListener addMouseListener() removeMouseListener() Component et dérivés*.
MouseEvent (55)(à la fois pour les clics et pour le déplacement) MouseMotionListener addMouseMotionListener() removeMouseMotionListener() Component et dérivés*.
WindowEvent WindowListener addWindowListener() removeWindowListener() Window et ses dérivés, comprenant JDialog, JFileDialog, and JFrame.
ItemEvent ItemListener addItemListener() removeItemListener() JCheckBox, JCheckBoxMenuItem, JComboBox, JList, et tout ce qui implémente l'interface ItemSelectable.
TextEvent TextListener addTextListener() removeTextListener() Tout ce qui est dérivé de JTextComponent, comprenant JTextArea et JTextField.

On voit que chaque type de composant ne supporte que certains types d'événements. Il semble assez difficile de rechercher tous les événements supportés par chaque composant. Une approche plus simple consiste à modifier le programme ShowMethodsClean.java du titre XIV de manière à ce qu'il affiche tous les event listeners supportés par tout composant Swing entré.

Le Titre XIV a introduit la réflexion et a utilisé cette fonctionnalité pour rechercher les méthodes d'une classe donnée, soit une liste complète des méthodes, soit un sous-ensemble des méthodes dont le nom contient un mot-clef donné. La magie dans ceci est qu'il peut automatiquement nous montrer toutes les méthodes d'une classe sans qu'on soit obligé de parcourir la hiérarchie des héritages en examinant les classes de base à chaque niveau. De ce fait, il fournit un outil précieux permettant de gagner du temps pour la programmation : comme les noms de la plupart des méthodes Java sont parlants et descriptifs, on peut rechercher les noms de méthodes contenant un mot particulier. Lorsqu'on pense avoir trouvé ce qu'on cherchait, il faut alors vérifier la documentation en ligne.

Comme dans le titre XIV on n'avait pas encore vu Swing, l'outil de ce chapitre était une application de ligne de commande. En voici une version plus pratique avec interface graphique, spécialisée dans la recherche des méthodes addListener dans les composants Swing :

 
Sélectionnez
//: c13:ShowAddListeners.java
// Affiche les méthodes "addXXXListener"
// d'une classe Swing donnée.
// <applet code = ShowAddListeners 
// width=500 height=400></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import java.io.*;
import com.bruceeckel.swing.*;
import com.bruceeckel.util.*;

public class ShowAddListeners extends JApplet {
  Class cl;
  Method[] m;
  Constructor[] ctor;
  String[] n = new String[0];
  JTextField name = new JTextField(25);
  JTextArea results = new JTextArea(40, 65);
  class NameL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String nm = name.getText().trim();
      if(nm.length() == 0) {
        results.setText("No match");
        n = new String[0];
        return;
      }
      try {
        cl = Class.forName("javax.swing." + nm);
      } catch(ClassNotFoundException ex) {
        results.setText("No match");
        return;
      }
      m = cl.getMethods();
      // Conversion en un tableau de Strings :
      n = new String[m.length];
      for(int i = 0; i         n[i] = m[i].toString();
      reDisplay();
    }
  } 
  void reDisplay() {
    // Creation de l'ensemble des résultats :
    String[] rs = new String[n.length];
    int j = 0;
    for (int i = 0; i       if(n[i].indexOf("add") != -1 &&
        n[i].indexOf("Listener") != -1)
          rs[j++] = 
            n[i].substring(n[i].indexOf("add"));
    results.setText("");
    for (int i = 0; i       results.append(
        StripQualifiers.strip(rs[i]) + "\n");
  }
  public void init() {
    name.addActionListener(new NameL());
    JPanel top = new JPanel();
    top.add(new JLabel(
      "Swing class name (press ENTER):"));
    top.add(name);
    Container cp = getContentPane();
    cp.add(BorderLayout.NORTH, top);
    cp.add(new JScrollPane(results));
  }
  public static void main(String[] args) {
    Console.run(new ShowAddListeners(), 500,400);
  }
} ///:~

La classe StripQualifiers définie au livre XIV est réutilisée ici en important la bibliothèque com.bruceeckel.util.

L'interface utilisateur graphique contient un JTextField name dans lequel on saisit le nom de la classe Swing à rechercher. Les résultats sont affichés dans une JTextArea.

On remarquera qu'il n'y a pas de boutons ou autres composants pour indiquer qu'on désire lancer la recherche. C'est parce que le JTextField est surveillé par un ActionListener. Lorsqu'on y fait un changement suivi de ENTER, la liste est immédiatement mise à jour. Si le texte n'est pas vide, il est utilisé dans Class.forName() pour rechercher la classe. Si le nom est incorrect, Class.forName() va échouer, c'est-à-dire qu'il va émettre une exception. Celle-ci est interceptée et le JTextArea est positionné à "No match". Mais si on tape un nom correct (les majuscules/minuscules comptent), Class.forName() réussit et getMethods() retourne un tableau d'objets Method. Chacun des objets du tableau est transformé en String à l'aide de toString() (cette méthode fournit la signature complète de la méthode) et ajoutée à n, un tableau de Strings. Le tableau n est un membre de la classe ShowAddListeners et est utilisé pour mettre à jour l'affichage chaque fois que reDisplay() est appelé.

reDisplay() crée un tableau de Strings appelé rs (pour "result set" : ensemble de résultats). L'ensemble des résultats est conditionnellement copié depuis les Strings de n qui contiennent add et Listener. indexOf() et substring() sont ensuite utilisés pour enlever les qualificatifs tels que public, static, etc. Enfin, StripQualifiers.strip() enlève les qualificatifs de noms.

Ce programme est une façon pratique de rechercher les capacités d'un composant Swing. Une fois connus les événements supportés par un composant donné, il n'y a pas besoin de rechercher autre chose pour réagir à cet événement. Il suffit de :

  1. Prendre le nom de la classe événement et retirer le mot Event. Ajouter le mot Listener à ce qui reste. Ceci donne le nom de l'interface listener qu'on doit implémenter dans une classe interne ;
  2. Implémenter l'interface ci-dessus et écrire les méthodes pour les événements qu'on veut intercepter. Par exemple, on peut rechercher les événements de déplacement de la souris, et on écrit donc le code pour la méthode mouseMoved() de l'interface MouseMotionListener (il faut également implémenter les autres méthodes, bien sûr, mais il y a souvent un raccourci que nous verrons bientôt) ;
  3. Créer un objet de la classe listener de l'étape 2. L'enregistrer avec le composant avec la méthode dont le nom est fourni en ajoutant add au début du nom du listener. Par exemple, addMouseMotionListener().

Voici quelques-unes des interfaces listeners :

Ce n'est pas une liste exhaustive, en partie du fait que le modèle d'événements nous permet de créer nos propres types d'événements et listeners associés. De ce fait, on rencontrera souvent des bibliothèques qui ont inventé leurs propres événements, et la connaissance acquise dans ce chapitre nous permettra de comprendre l'utilisation de ces événements.

XV-G-1-a. Utilisation de listener adapters pour simplifier

Dans le tableau ci-dessus, on peut voir que certaines interfaces listener ne possèdent qu'une seule méthode. Celles-ci sont triviales à implémenter puisqu'on ne les implémentera que lorsqu'on désire écrire cette méthode particulière. Par contre, les interfaces listener qui ont plusieurs méthodes peuvent être moins agréables à utiliser. Par exemple, quelque chose qu'il faut toujours faire en créant une application est de fournir un WindowListener au JFrame de manière à pouvoir appeler System.exit() pour sortir de l'application lorsqu'on reçoit l'événement windowClosing(). Mais comme WindowListener est une interface, il faut implémenter chacune de ses méthodes même si elles ne font rien. Cela peut être ennuyeux.

Pour résoudre le problème, certaines (mais pas toutes) des interfaces listener qui ont plus d'une méthode possèdent des adaptateurs [adapters], dont vous pouvez voir les noms dans le tableau ci-dessus. Chaque adaptateur fournit des méthodes vides par défaut pour chacune des méthodes de l'interface. Ensuite il suffit d'hériter de cet adaptateur et de redéfinir uniquement les méthodes qu'on doit modifier. Par exemple, le WindowListener qu'on utilisera normalement ressemble à ceci (souvenez-vous qu'il a été encapsulé dans la classe Console de com.bruceeckel.swing) :

 
Sélectionnez
class MyWindowListener extends WindowAdapter {
  public void windowClosing(WindowEvent e) {
System.exit(0);
}
}

Le seul but des adaptateurs est de faciliter la création des classes listener.

Il y a cependant un désavantage lié aux adaptateurs, sous la forme d'un piège. Supposons qu'on écrive un WindowAdapter comme celui ci-dessus :

 
Sélectionnez
class MyWindowListener extends WindowAdapter {
  public void WindowClosing(WindowEvent e) {
System.exit(0);
}
}

Ceci ne marche pas, mais il nous rendra fous à comprendre pourquoi, car tout va compiler et s'exécuter correctement, sauf que la fermeture de la fenêtre ne fera pas sortir du programme. Voyez-vous le problème ? Il est situé dans le nom de la méthode : WindowClosing() au lieu de windowClosing(). Une simple erreur de majuscule se traduit par l'ajout d'une méthode nouvelle. Ce n'est cependant pas cette méthode qui est appelée lorsque la fenêtre est fermée, de sorte qu'on n'obtient pas le résultat attendu. En dépit de cet inconvénient, une interface garantit que les méthodes sont correctement implémentées.

XV-G-2. Surveiller plusieurs événements

Pour nous prouver que ces événements sont bien déclenchés, et en tant qu'expérience intéressante, créons une applet qui surveille les autres comportements d'un JButton, autres que le simple fait qu'il soit appuyé ou pas. Cet exemple montre également comment hériter de notre propre objet bouton, car c'est ce qui est utilisé comme cible de tous les événements intéressants. Pour cela, il suffit d'hériter de JButton name="fnB69" href="#fn69" (56).

La classe MyButton est une classe interne de TrackEvent, de sorte que MyButton peut aller dans la fenêtre parent et manipuler ses champs textes, ce qu'il faut pour pouvoir écrire une information d'état dans les champs du parent. Bien sûr ceci est une solution limitée, puisque MyButton peut être utilisée uniquement avec TrackEvent. Ce genre de code est parfois appelé "fortement couplé" :

 
Sélectionnez
//: c13:TrackEvent.java
// Montre les événements lorsqu'ils arrivent.
// <applet code=TrackEvent
//  width=700 height=500></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;

public class TrackEvent extends JApplet {
HashMap h = new HashMap();
String[] event = {
   "focusGained", "focusLost", "keyPressed",
   "keyReleased", "keyTyped", "mouseClicked",
   "mouseEntered", "mouseExited","mousePressed",
   "mouseReleased", "mouseDragged", "mouseMoved"
};
MyButton
b1 = new MyButton(Color.blue, "test1"),
b2 = new MyButton(Color.red, "test2");
  class MyButton extends JButton {
   void report(String field, String msg) {
((JTextField)h.get(field)).setText(msg);
}    
FocusListener fl = new FocusListener() {
    public void focusGained(FocusEvent e) {
   report("focusGained", e.paramString());
}
    public void focusLost(FocusEvent e) {
   report("focusLost", e.paramString());
}
};
KeyListener kl = new KeyListener() {
    public void keyPressed(KeyEvent e) {
   report("keyPressed", e.paramString());
}
    public void keyReleased(KeyEvent e) {
   report("keyReleased", e.paramString());
}
    public void keyTyped(KeyEvent e) {
   report("keyTyped", e.paramString());
}
};
MouseListener ml = new MouseListener() {
    public void mouseClicked(MouseEvent e) {
   report("mouseClicked", e.paramString());
}
    public void mouseEntered(MouseEvent e) {
   report("mouseEntered", e.paramString());
}
    public void mouseExited(MouseEvent e) {
   report("mouseExited", e.paramString());
}
    public void mousePressed(MouseEvent e) {
   report("mousePressed", e.paramString());
}
    public void mouseReleased(MouseEvent e) {
   report("mouseReleased", e.paramString());
}
};
MouseMotionListener mml = 
    new MouseMotionListener() {
    public void mouseDragged(MouseEvent e) {
   report("mouseDragged", e.paramString());
}
    public void mouseMoved(MouseEvent e) {
   report("mouseMoved", e.paramString());
}
};
   public MyButton(Color color, String label) {
    super(label);
setBackground(color);
addFocusListener(fl);
addKeyListener(kl);
addMouseListener(ml);
addMouseMotionListener(mml);
}
}  
  public void init() {
Container c = getContentPane();
c.setLayout(new GridLayout(event.length+1,2));
   for(int i = 0; i JTextField t = new JTextField();
t.setEditable(false);
c.add(new JLabel(event[i], JLabel.RIGHT));
c.add(t);
h.put(event[i], t);
}
c.add(b1);
c.add(b2);
}
  public static void main(String[] args) {
Console.run(new TrackEvent(), 700, 500);
}
} ///:~

Dans le constructeur de MyButton, la couleur des boutons est positionnée par un appel à SetBackground(). Les listeners sont tous installés par de simples appels de méthodes.

La classe TrackEvent contient une HashMap pour contenir les chaînes représentant le type d'événement et les JTextFields dans lesquels l'information sur cet événement est conservée. Bien sûr, ceux-ci auraient pu être créés en statique plutôt qu'en les mettant dans une HashMap, mais je pense que vous serez d'accord que c'est beaucoup plus facile à utiliser et modifier. En particulier, si on a besoin d'ajouter ou supprimer un nouveau type d'événement dans TrackEvent, il suffit d'ajouter ou supprimer une chaîne dans le tableau event, et tout le reste est automatique.

Lorsque report() est appelé, on lui donne le nom de l'événement et la chaîne des paramètres de cet événement. Il utilise le HashMap h de la classe externe pour rechercher le JTextField associé à l'événement portant ce nom, et place alors la chaîne des paramètres dans ce champ.

Cet exemple est amusant à utiliser, car on peut réellement voir ce qui se passe avec les événements dans son programme.

XV-H. Un catalogue de composants Swing

Maintenant que nous connaissons les layout managers et le modèle d'événements, nous sommes prêts pour voir comment utiliser les composants Swing. Cette section est une visite non exhaustive des composants Swing et des fonctionnalités que vous utiliserez probablement la plupart du temps. Chaque exemple est conçu pour être de taille raisonnable de manière à pouvoir facilement récupérer le code dans d'autres programmes.

Vos pouvez facilement voir à quoi ressemble chacun de ces exemples en fonctionnement, en visualisant les pages HTML dans le code source téléchargeable de ce chapitre.

Gardez en tête :

  1. La documentation HTML de java.sun.com comprend toutes les classes et méthodes de Swing (seules quelques-unes sont montrées ici) ;
  2. Grâce aux conventions de nommage utilisées pour les événements Swing, il est facile de deviner comment écrire et installer un gestionnaire d'un événement de type donné. Utilisez le programme de recherche ShowAddListeners.java introduit plus avant dans ce chapitre pour faciliter votre investigation d'un composant particulier ;
  3. Lorsque les choses deviendront compliquées, passez à un GUI builder.

XV-H-1. Boutons

Swing comprend un certain nombre de boutons de différents types. Tous les boutons, boîtes à cocher [check boxes], boutons radio [radio buttons], et même les éléments de menus [menu items] héritent de AbstractButton(qui, vu qu'ils comprennent les éléments de menus, auraient probablement été mieux nommés AbstractChooser ou quelque chose du genre). Nous verrons l'utilisation des éléments de menus bientôt, mais l'exemple suivant montre les différents types de boutons existants :

 
Sélectionnez
//: c13:Buttons.java
// Divers boutons Swing.
// <applet code=Buttons
//  width=350 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;

public class Buttons extends JApplet {
JButton jb = new JButton("JButton");
BasicArrowButton
up = new BasicArrowButton(
BasicArrowButton.NORTH),
down = new BasicArrowButton(
BasicArrowButton.SOUTH),
right = new BasicArrowButton(
BasicArrowButton.EAST),
left = new BasicArrowButton(
BasicArrowButton.WEST);
  public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(jb);
cp.add(new JToggleButton("JToggleButton"));
cp.add(new JCheckBox("JCheckBox"));
cp.add(new JRadioButton("JRadioButton"));
JPanel jp = new JPanel();
jp.setBorder(new TitledBorder("Directions"));
jp.add(up);
jp.add(down);
jp.add(left);
jp.add(right);
cp.add(jp);
}
  public static void main(String[] args) {
Console.run(new Buttons(), 350, 100);
}
} ///:~

On commence par le BasicArrowButtonde javax.swing.plaf.basic, puis on continue avec les divers types de boutons. Si vous exécutez cet exemple, vous verrez que le toggle button (bouton inverseur) garde sa dernière position, enfoncé ou relâché. Mais les boîtes à cocher et les boutons radio se comportent de manière identique, on les clique pour les (dé)sélectionner (ils sont hérités de JToggleButton).

XV-H-1-a. Groupes de boutons

Si on désire des boutons radio qui se comportent selon un "ou exclusif", il faut les ajouter à un groupe de boutons. Mais, comme l'exemple ci-dessous le montre, tout AbstractButton peut être ajouté à un ButtonGroup.

Pour éviter de répéter beaucoup de code, cet exemple utilise la réflexion pour générer les groupes de différents types de boutons. Ceci peut se voir dans makeBPanel(), qui crée un groupe de boutons et un JPanel. Le second argument de makeBPanel() est un tableau de String. Pour chaque String, un bouton de la classe désignée par le premier argument est ajouté au JPanel :

 
Sélectionnez
//: c13:ButtonGroups.java
// Utilise la réflexion pour créer des groupes 
// de différents types de AbstractButton.
// <applet code=ButtonGroups
//  width=500 height=300></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import java.lang.reflect.*;
import com.bruceeckel.swing.*;

public class ButtonGroups extends JApplet {
  static String[] ids = { 
   "June", "Ward", "Beaver", 
   "Wally", "Eddie", "Lumpy",
};
  static JPanel 
makeBPanel(Class bClass, String[] ids) {
ButtonGroup bg = new ButtonGroup();
JPanel jp = new JPanel();
String title = bClass.getName();
title = title.substring(
title.lastIndexOf('.') + 1);
jp.setBorder(new TitledBorder(title));
   for(int i = 0; i AbstractButton ab = new JButton("failed");
    try {
     // Obtient la méthode de construction dynamique
     // qui demande un argument String :
   Constructor ctor = bClass.getConstructor(
      new Class[] { String.class });
     // Creation d'un nouvel objet :
   ab = (AbstractButton)ctor.newInstance(
      new Object[]{ids[i]});
} catch(Exception ex) {
   System.err.println("can't create " + 
     bClass);
}
bg.add(ab);
jp.add(ab);
}
   return jp;
}
  public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(makeBPanel(JButton.class, ids));
cp.add(makeBPanel(JToggleButton.class, ids));
cp.add(makeBPanel(JCheckBox.class, ids));
cp.add(makeBPanel(JRadioButton.class, ids));
}
  public static void main(String[] args) {
Console.run(new ButtonGroups(), 500, 300);
}
} ///:~

Ceci ajoute un peu de complexité à ce qui est un processus simple. Pour obtenir un comportement de "OU exclusif" avec des boutons, on crée un groupe de boutons et on ajoute au groupe chaque bouton pour lequel on désire ce comportement. Lorsqu'on exécute le programme, on voit que tous les boutons, à l'exception de JButton, montrent ce comportement de "OU exclusif".

XV-H-2. Icônes

On peut utiliser un Icon dans un JLabel ou tout ce qui hérite de AbstractButton (y compris JButton, JCheckBox, JRadioButton, et les différents types de JMenuItem). L'utilisation d'Icons avec des JLabels est assez directe (on verra un exemple plus tard). L'exemple suivant explore toutes les façons d'utiliser des Icons avec des boutons et leurs descendants.

Vous pouvez utiliser les fichiers gif que vous voulez, mais ceux utilisés dans cet exemple font partie de la livraison du code de ce livre, disponible à www.BruceEckel.com. Pour ouvrir un fichier et utiliser l'image, il suffit de créer un ImageIcon et de lui fournir le nom du fichier. À partir de là on peut utiliser l'Icon obtenu dans le programme.

Remarquez que l'information de chemin est codée en dur dans cet exemple ; vous devrez changer ce chemin pour qu'il corresponde à l'emplacement des fichiers des images.

 
Sélectionnez
//: c13:Faces.java
// Comportement des Icones dans des  Jbuttons.
// <applet code=Faces
//  width=250 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Faces extends JApplet {
  // L'information de chemin suivante est nécessaire
  // pour l'exécution via une applet directement depuis le disque :
  static String path = 
   "C:/aaa-TIJ2-distribution/code/c13/";
  static Icon[] faces = {
   new ImageIcon(path + "face0.gif"),
   new ImageIcon(path + "face1.gif"),
   new ImageIcon(path + "face2.gif"),
   new ImageIcon(path + "face3.gif"),
   new ImageIcon(path + "face4.gif"),
};
JButton 
jb = new JButton("JButton", faces[3]),
jb2 = new JButton("Disable");
  boolean mad = false;
  public void init() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
jb.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e){
     if(mad) {
     jb.setIcon(faces[3]);
     mad = false;
   } else {
     jb.setIcon(faces[0]);
     mad = true;
   }
   jb.setVerticalAlignment(JButton.TOP);
   jb.setHorizontalAlignment(JButton.LEFT);
}
});
jb.setRolloverEnabled(true);
jb.setRolloverIcon(faces[1]);
jb.setPressedIcon(faces[2]);
jb.setDisabledIcon(faces[4]);
jb.setToolTipText("Yow!");
cp.add(jb);
jb2.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e){
     if(jb.isEnabled()) {
     jb.setEnabled(false);
     jb2.setText("Enable");
   } else {
     jb.setEnabled(true);
     jb2.setText("Disable");
   }
}
});
cp.add(jb2);
}
  public static void main(String[] args) {
Console.run(new Faces(), 400, 200);
}
} ///:~

Un Icon peut être utilisé dans de nombreux constructeurs, mais on peut aussi utiliser setIcon() pour ajouter ou changer un Icon. Cet exemple montre également comment un JButton (ou un quelconque AbstractButton) peut positionner les différentes sortes d'icônes qui apparaissent lorsque des choses se passent sur ce bouton : lorsqu'il est enfoncé, invalidé, ou lorsqu'on roule par-dessus [rolled over] (la souris passe au-dessus sans cliquer). On verra que ceci donne au bouton une sensation d'animation agréable.

XV-H-3. Infobulles [Tooltips]

L'exemple précédent ajoutait un tool tip au bouton. La plupart des classes qu'on utilisera pour créer une interface utilisateur sont dérivées de JComponent, qui contient une méthode appelée setToolTipText(String). Donc pratiquement pour tout ce qu'on place sur un formulaire, il suffit de dire (pour un objet jc de toute classe dérivée de JComponent) :

 
Sélectionnez
jc.setToolTipText("My tip");

et lorsque la souris reste au-dessus de ce JComponent pour un temps prédéterminé, une petite boîte contenant le texte va apparaître à côté de la souris.

XV-H-4. Champs de texte [Text Fields]

Cet exemple montre le comportement supplémentaire dont sont capables les JTextFields :

 
Sélectionnez
//: c13:TextFields.java
// Champs de texte et événements Java.
// <applet code=TextFields width=375
// height=125></applet>
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class TextFields extends JApplet {
JButton
b1 = new JButton("Get Text"),
b2 = new JButton("Set Text");
JTextField
t1 = new JTextField(30),
t2 = new JTextField(30),
t3 = new JTextField(30);
String s = new String();
UpperCaseDocument
ucd = new UpperCaseDocument();
  public void init() {
t1.setDocument(ucd);
ucd.addDocumentListener(new T1());
b1.addActionListener(new B1());
b2.addActionListener(new B2());
DocumentListener dl = new T1();
t1.addActionListener(new T1A());
Container cp = getContentPane();
cp.setLayout(new FlowLayout());
cp.add(b1);
cp.add(b2);
cp.add(t1);
cp.add(t2);
cp.add(t3);
}
  class T1 implements DocumentListener {
   public void changedUpdate(DocumentEvent e){}
   public void insertUpdate(DocumentEvent e){
t2.setText(t1.getText());
t3.setText("Text: "+ t1.getText());
}
   public void removeUpdate(DocumentEvent e){
t2.setText(t1.getText());
}
}
  class T1A implements ActionListener {
   private int count = 0;
   public void actionPerformed(ActionEvent e) {
t3.setText("t1 Action Event " + count++);
}
}
  class B1 implements ActionListener {
   public void actionPerformed(ActionEvent e) {
    if(t1.getSelectedText() == null)
   s = t1.getText();
    else
   s = t1.getSelectedText();
t1.setEditable(true);
}
}
  class B2 implements ActionListener {
   public void actionPerformed(ActionEvent e) {
ucd.setUpperCase(false);
t1.setText("Inserted by Button 2: " + s);
ucd.setUpperCase(true);
t1.setEditable(false);
}
}
  public static void main(String[] args) {
Console.run(new TextFields(), 375, 125);
}
}

class UpperCaseDocument extends PlainDocument {
  boolean upperCase = true;
  public void setUpperCase(boolean flag) {
upperCase = flag;
}
  public void insertString(int offset, 
String string, AttributeSet attributeSet)
   throws BadLocationException {
    if(upperCase)
   string = string.toUpperCase();
    super.insertString(offset, 
   string, attributeSet);
}
} ///:~

Le JTextField t3 est inclus pour servir d'emplacement pour signaler lorsque l'action listener du JTextField t1 est lancé. On verra que l'action listener d'un JTextField n'est lancée que lorsqu'on appuie sur la touche enter.

Le JTextField t1 a plusieurs listeners attachés. le listener T1 est un Document Listener qui répond à tout changement dans le document (le contenu du JTextField, dans ce cas). Il copie automatiquement tout le texte de t1 dans t2. De plus, le document t1 est positionné à une classe dérivée de PlainDocument, appelée UpperCaseDocument, qui force tous les caractères en majuscules. Il détecte automatiquement les retours en arrière [backspaces] et effectue l'effacement, tout en ajustant le curseur et gérant tout de la manière attendue.

XV-H-5. Bordures

JComponent possède une méthode appelée setBorder(), qui permet de placer différentes bordures intéressantes sur tout composant visible. L'exemple suivant montre certaines des bordures existantes, en utilisant la méthode showBorder() qui crée un JPanel et lui attache une bordure à chaque fois. Il utilise aussi RTTI pour trouver le nom de la bordure qu'on utilise (en enlevant l'information du chemin), et met ensuite le nom dans un JLabel au milieu du panneau :

 
Sélectionnez
//: c13:Borders.java
// Diverses bordures Swing.
// <applet code=Borders
//  width=500 height=300></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;

public class Borders extends JApplet {
  static JPanel showBorder(Border b) {
    JPanel jp = new JPanel();
    jp.setLayout(new BorderLayout());
    String nm = b.getClass().toString();
    nm = nm.substring(nm.lastIndexOf('.') + 1);
    jp.add(new JLabel(nm, JLabel.CENTER), 
      BorderLayout.CENTER);
    jp.setBorder(b);
    return jp;
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.setLayout(new GridLayout(2,4));
    cp.add(showBorder(new TitledBorder("Title")));
    cp.add(showBorder(new EtchedBorder()));
    cp.add(showBorder(new LineBorder(Color.blue)));
    cp.add(showBorder(
      new MatteBorder(5,5,30,30,Color.green)));
    cp.add(showBorder(
      new BevelBorder(BevelBorder.RAISED)));
    cp.add(showBorder(
      new SoftBevelBorder(BevelBorder.LOWERED)));
    cp.add(showBorder(new CompoundBorder(
      new EtchedBorder(),
      new LineBorder(Color.red))));
  }
  public static void main(String[] args) {
    Console.run(new Borders(), 500, 300);
  }
} ///:~

On peut également créer ses propres bordures et les placer dans des boutons, labels, etc. tout ce qui est dérivé de JComponent.

XV-H-6. JScrollPanes

La plupart du temps on laissera le JScrollPane tel quel, mais on peut aussi contrôler quelles barres de défilement sont autorisées, verticales, horizontales, les deux ou ni l'une ni l'autre :

 
Sélectionnez
//: c13:JScrollPanes.java
// Contrôle des scrollbars d'un JScrollPane.
// <applet code=JScrollPanes width=300 height=725>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;

public class JScrollPanes extends JApplet {
  JButton 
    b1 = new JButton("Text Area 1"),
    b2 = new JButton("Text Area 2"),
    b3 = new JButton("Replace Text"),
    b4 = new JButton("Insert Text");
  JTextArea 
    t1 = new JTextArea("t1", 1, 20),
    t2 = new JTextArea("t2", 4, 20),
    t3 = new JTextArea("t3", 1, 20),
    t4 = new JTextArea("t4", 10, 10),
    t5 = new JTextArea("t5", 4, 20),
    t6 = new JTextArea("t6", 10, 10);
  JScrollPane 
    sp3 = new JScrollPane(t3,
      JScrollPane.VERTICAL_SCROLLBAR_NEVER,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER),
    sp4 = new JScrollPane(t4,
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER),
    sp5 = new JScrollPane(t5,
      JScrollPane.VERTICAL_SCROLLBAR_NEVER,
      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS),
    sp6 = new JScrollPane(t6,
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
  class B1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t5.append(t1.getText() + "\n");
    }
  }
  class B2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.setText("Inserted by Button 2");
      t2.append(": " + t1.getText());
      t5.append(t2.getText() + "\n");
    }
  }
  class B3L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String s = " Replacement ";
      t2.replaceRange(s, 3, 3 + s.length());
    }
  }
  class B4L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.insert(" Inserted ", 10);
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    // Création de Borders pour les composants:
    Border brd = BorderFactory.createMatteBorder(
      1, 1, 1, 1, Color.black);
    t1.setBorder(brd);
    t2.setBorder(brd);
    sp3.setBorder(brd);
    sp4.setBorder(brd);
    sp5.setBorder(brd);
    sp6.setBorder(brd);
    // Initialisation des listeners et ajout des composants:
    b1.addActionListener(new B1L());
    cp.add(b1);
    cp.add(t1);
    b2.addActionListener(new B2L());
    cp.add(b2);
    cp.add(t2);
    b3.addActionListener(new B3L());
    cp.add(b3);
    b4.addActionListener(new B4L());
    cp.add(b4);
    cp.add(sp3); 
    cp.add(sp4); 
    cp.add(sp5);
    cp.add(sp6);
  }
  public static void main(String[] args) {
    Console.run(new JScrollPanes(), 300, 725);
  }
} ///:~

L'utilisation des différents arguments du constructeur de JScrollPane contrôle la présence des scrollbars. Cet exemple est également un peu "habillé" à l'aide de bordures.

XV-H-7. Un miniéditeur

Le contrôle JTextPane fournit un support important pour l'édition de texte, sans grand effort. L'exemple suivant en fait une utilisation très simple, en ignorant le plus gros des fonctionnalités de la classe :

 
Sélectionnez
//: c13:TextPane.java
// Le contrôle JTextPane est un petit éditeur de texte.
// <applet code=TextPane width=475 height=425>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
import com.bruceeckel.util.*;

public class TextPane extends JApplet {
  JButton b = new JButton("Add Text");
  JTextPane tp = new JTextPane();
  static Generator sg = 
    new Arrays2.RandStringGenerator(7);  
  public void init() {
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        for(int i = 1; i           tp.setText(tp.getText() + 
            sg.next() + "\n");
      }
    });
    Container cp = getContentPane();
    cp.add(new JScrollPane(tp));
    cp.add(BorderLayout.SOUTH, b);
  }
  public static void main(String[] args) {
    Console.run(new TextPane(), 475, 425);
  }
} ///:~

Le bouton ajoute simplement au hasard du texte généré. Le but du JTextPane est de permettre la modification de texte sur place, de sorte qu'on ne trouvera pas de méthode append(). Dans ce cas-ci (il est admis qu'il s'agit d'un piètre usage des capacités de JTextPane), le texte doit être saisi, modifié, et replacé dans le panneau en utilisant setText().

Remarquez les fonctionnalités intrinsèques du JTextPane, telles que le retour à la ligne automatique. Il y a de nombreuses autres fonctionnalités à découvrir dans la documentation du JDK.

XV-H-8. Boîtes à cocher [Check boxes]

Une boîte à cocher [check box] permet d'effectuer un choix simple vrai/faux ; il consiste en une petite boîte et un label. La boîte contient d'habitude un petit x (ou tout autre moyen d'indiquer qu'elle est cochée) ou est vide, selon qu'elle a été sélectionnée ou non.

On crée normalement une JCheckBox en utilisant un constructeur qui prend le label comme argument. On peut obtenir ou forcer l'état, et également obtenir ou forcer le label si on veut le lire ou le modifier après la création de la JCheckBox.

Chaque fois qu'une JCheckBox est remplie ou vidée, un événement est généré, qu'on peut capturer de la même façon que pour un bouton, en utilisant un ActionListener. L'exemple suivant utilise un JTextArea pour lister toutes les boîtes à cocher qui ont été cochées :

 
Sélectionnez
//: c13:CheckBoxes.java
// Utilisation des JCheckBoxes.
// <applet code=CheckBoxes width=200 height=200>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class CheckBoxes extends JApplet {
  JTextArea t = new JTextArea(6, 15);
  JCheckBox 
    cb1 = new JCheckBox("Check Box 1"),
    cb2 = new JCheckBox("Check Box 2"),
    cb3 = new JCheckBox("Check Box 3");
  public void init() {
    cb1.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        trace("1", cb1);
      }
    });
    cb2.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        trace("2", cb2);
      }
    });
    cb3.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        trace("3", cb3);
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(new JScrollPane(t));
    cp.add(cb1); 
    cp.add(cb2); 
    cp.add(cb3);
  }
  void trace(String b, JCheckBox cb) {
    if(cb.isSelected())
      t.append("Box " + b + " Set\n");
    else
      t.append("Box " + b + " Cleared\n");
  }
  public static void main(String[] args) {
    Console.run(new CheckBoxes(), 200, 200);
  }
} ///:~

La méthode trace() envoie le nom et l'état de la JCheckBox sélectionnée au JTextArea en utilisant append(), de telle sorte qu'on voit une liste cumulative des boîtes à cocher qui ont été sélectionnées, et quel est leur état.

XV-H-9. Boutons radio

Le concept d'un bouton radio en programmation de GUI provient des autoradios d'avant l'ère électronique, avec des boutons mécaniques : quand on appuie sur l'un d'eux, tout autre bouton enfoncé est relâché. Ceci permet de forcer un choix unique parmi plusieurs.

Pour installer un groupe de JRadioButtons, il suffit de les ajouter à un ButtonGroup (il peut y avoir un nombre quelconque de ButtonGroups dans un formulaire). En utilisant le second argument du constructeur, on peut optionnellement forcer à true l'état d'un des boutons. Si on essaie de forcer à true plus d'un bouton radio, seul le dernier forcé sera à true.

Voici un exemple simple d'utilisation de boutons radio. On remarquera que les événements des boutons radio s'interceptent comme tous les autres :

 
Sélectionnez
//: c13:RadioButtons.java
// Utilisation des JRadioButtons.
// <applet code=RadioButtons 
// width=200 height=100> </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class RadioButtons extends JApplet {
  JTextField t = new JTextField(15);
  ButtonGroup g = new ButtonGroup();
  JRadioButton 
    rb1 = new JRadioButton("one", false),
    rb2 = new JRadioButton("two", false),
    rb3 = new JRadioButton("three", false);
  ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      t.setText("Radio button " + 
        ((JRadioButton)e.getSource()).getText());
    }
  };
  public void init() {
    rb1.addActionListener(al);
    rb2.addActionListener(al);
    rb3.addActionListener(al);
    g.add(rb1); g.add(rb2); g.add(rb3);
    t.setEditable(false);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t); 
    cp.add(rb1); 
    cp.add(rb2); 
    cp.add(rb3); 
  }
  public static void main(String[] args) {
    Console.run(new RadioButtons(), 200, 100);
  }
} ///:~

Pour afficher l'état un champ texte est utilisé. Ce champ est déclaré non modifiable, car il est utilisé uniquement pour afficher des données et pas pour en recevoir. C'est une alternative à l'utilisation d'un JLabel.

XV-H-10. Boîtes combo (listes à ouverture vers le bas) [combo boxes (drop-down lists)]

Comme les groupes de boutons radio, une drop-down list est une façon de forcer l'utilisateur à choisir un seul élément parmi un groupe de possibilités. C'est cependant un moyen plus compact, et il est plus facile de modifier les éléments de la liste sans surprendre l'utilisateur (on peut modifier dynamiquement les boutons radio, mais ça peut devenir visuellement perturbant).

La JComboBox java n'est pas comme la combo box de Windows qui permet de sélectionner dans une liste ou de taper soi-même une sélection. Avec une JComboBox on choisit un et un seul élément de la liste. Dans l'exemple suivant, la JComboBox démarre avec un certain nombre d'éléments et ensuite des éléments sont ajoutés lorsqu'on appuie sur un bouton.

 
Sélectionnez
//: c13:ComboBoxes.java
// Utilisation des drop-down lists.
// <applet code=ComboBoxes
// width=200 height=100> </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class ComboBoxes extends JApplet {
  String[] description = { "Ebullient", "Obtuse",
    "Recalcitrant", "Brilliant", "Somnescent",
    "Timorous", "Florid", "Putrescent" };
  JTextField t = new JTextField(15);
  JComboBox c = new JComboBox();
  JButton b = new JButton("Add items");
  int count = 0;
  public void init() {
    for(int i = 0; i       c.addItem(description[count++]);
    t.setEditable(false);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        if(count           c.addItem(description[count++]);
      }
    });
    c.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        t.setText("index: "+ c.getSelectedIndex()
          + "   " + ((JComboBox)e.getSource())
          .getSelectedItem());
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    cp.add(c);
    cp.add(b);
  }
  public static void main(String[] args) {
    Console.run(new ComboBoxes(), 200, 100);
  }
} ///:~

Le JTextField affiche l'index sélectionné, qui est le numéro séquentiel de l'élément sélectionné, ainsi que le label du bouton radio.

XV-H-11. Listes [List boxes]

Les listes sont différentes des JComboBox, et pas seulement en apparence. Alors qu'une JComboBox s'affiche vers le bas lorsqu'on l'active, une JList occupe un nombre fixe de lignes sur l'écran tout le temps et ne se modifie pas. Si on veut voir les éléments de la liste, il suffit d'appeler getSelectedValues(), qui retourne un tableau de String des éléments sélectionnés.

Une JList permet la sélection multiple : si on "control-clique" sur plus d'un élément (en enfonçant la touche control tout en effectuant des clics souris) l'élément initial reste surligné et on peut en sélectionner autant qu'on veut. Si on sélectionne un élément puis qu'on en "shift-clique" un autre, tous les éléments entre ces deux-là seront aussi sélectionnés. Pour supprimer un élément d'un groupe on peut le "control-cliquer".

 
Sélectionnez
//: c13:List.java
// <applet code=List width=250
// height=375> </applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;

public class List extends JApplet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip",
    "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie" };
  DefaultListModel lItems=new DefaultListModel();
  JList lst = new JList(lItems);
  JTextArea t = new JTextArea(flavors.length,20);
  JButton b = new JButton("Add Item");
  ActionListener bl = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      if(count         lItems.add(0, flavors[count++]);
      } else {
        // Invalider, puisqu'il n'y a plus
        // de parfums à ajouter à la liste
        b.setEnabled(false);
      }
    }
  };
  ListSelectionListener ll =    new ListSelectionListener() {
      public void valueChanged(
        ListSelectionEvent e) {
          t.setText("");
          Object[] items=lst.getSelectedValues();
          for(int i = 0; i             t.append(items[i] + "\n");
        }
    };
  int count = 0;
  public void init() {
    Container cp = getContentPane();
    t.setEditable(false);
    cp.setLayout(new FlowLayout());
    // Création de Bords pour les composants:
    Border brd = BorderFactory.createMatteBorder(
      1, 1, 2, 2, Color.black);
    lst.setBorder(brd);
    t.setBorder(brd);
    // Ajout des quatre premiers éléments à la liste
    for(int i = 0; i       lItems.addElement(flavors[count++]);
    // Ajout des éléments au Content Pane pour affichage
    cp.add(t);
    cp.add(lst);
    cp.add(b);
    // Enregistrement des listeners d'événements
    lst.addListSelectionListener(ll);
    b.addActionListener(bl);
  }
  public static void main(String[] args) {
    Console.run(new List(), 250, 375);
  }
} ///:~

Quand on appuie sur le bouton, il ajoute des éléments au début de la liste (car le second argument de addItem() est 0).

On peut également voir qu'une bordure a été ajoutée aux listes.

Si on veut simplement mettre un tableau de Strings dans une JList, il y a une solution beaucoup plus simple : passer le tableau au constructeur de la JList, et il construit la liste automatiquement. La seule raison d'utiliser le modèle de liste de l'exemple ci-dessus est que la liste peut être manipulée lors de l'exécution du programme.

Les JLists ne fournissent pas de support direct pour le scrolling. Bien évidemment, il suffit d'encapsuler la JList dans une JScrollPane et tous les détails sont automatiquement gérés.

XV-H-12. Panneaux à tabulations [Tabbed panes]

Les JTabbedPane permettent de créer un dialogue tabulé, avec sur un côté des tabulations semblables à celles de classeurs de fiches, permettant d'amener au premier plan un autre dialogue en cliquant sur l'une d'entre elles.

 
Sélectionnez
//: c13:TabbedPane1.java
// Démonstration de Tabbed Pane.
// <applet code=TabbedPane1 
// width=350 height=200> </applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class TabbedPane1 extends JApplet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip", 
    "Mocha Almond Fudge", "Rum Raisin", 
    "Praline Cream", "Mud Pie" };
  JTabbedPane tabs = new JTabbedPane();
  JTextField txt = new JTextField(20);
  public void init() {
    for(int i = 0; i       tabs.addTab(flavors[i], 
        new JButton("Tabbed pane " + i));
    tabs.addChangeListener(new ChangeListener(){
      public void stateChanged(ChangeEvent e) {
        txt.setText("Tab selected: " + 
          tabs.getSelectedIndex());
      }
    });
    Container cp = getContentPane();
    cp.add(BorderLayout.SOUTH, txt);
    cp.add(tabs);
  }
  public static void main(String[] args) {
    Console.run(new TabbedPane1(), 350, 200);
  }
} ///:~

En Java, l'utilisation d'un mécanisme de panneaux à tabulations est importante, car pour la programmation d'applets, l'utilisation de dialogues pop-ups est découragée par l'apparition automatique d'un petit avertissement à chaque dialogue qui surgit d'une applet.

Lors de l'exécution de ce programme on remarquera que le JTabbedPane empile automatiquement les tabulations s'il y en a trop pour une rangée. On peut s'en apercevoir en redimensionnant la fenêtre lorsque le programme est lancé depuis la ligne de commande.

XV-H-13. Boîtes de messages

Les environnements de fenêtrage comportent classiquement un ensemble standard de boîtes de messages qui permettent d'afficher rapidement une information à l'utilisateur, ou lui demander une information. Dans Swing, ces boîtes de messages sont contenues dans les JOptionPanes. Il y a de nombreuses possibilités (certaines assez sophistiquées), mais celles qu'on utilise le plus couramment sont les messages et les confirmations, appelées en utilisant static JOptionPane.showMessageDialog() et JOptionPane.showConfirmDialog(). L'exemple suivant montre un sous-ensemble des boîtes de messages disponibles avec JOptionPane :

 
Sélectionnez
//: c13:MessageBoxes.java
// Démonstration de JoptionPane.
// <applet code=MessageBoxes 
// width=200 height=150> </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class MessageBoxes extends JApplet {
  JButton[] b = { new JButton("Alert"), 
    new JButton("Yes/No"), new JButton("Color"),
    new JButton("Input"), new JButton("3 Vals")
  };
  JTextField txt = new JTextField(15);
  ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e){
      String id = 
        ((JButton)e.getSource()).getText();
      if(id.equals("Alert"))
        JOptionPane.showMessageDialog(null, 
          "There's a bug on you!", "Hey!", 
          JOptionPane.ERROR_MESSAGE);
      else if(id.equals("Yes/No"))
        JOptionPane.showConfirmDialog(null, 
          "or no", "choose yes", 
          JOptionPane.YES_NO_OPTION);
      else if(id.equals("Color")) {
        Object[] options = { "Red", "Green" };
        int sel = JOptionPane.showOptionDialog(
          null, "Choose a Color!", "Warning", 
          JOptionPane.DEFAULT_OPTION, 
          JOptionPane.WARNING_MESSAGE, null, 
          options, options[0]);
          if(sel != JOptionPane.CLOSED_OPTION)
            txt.setText(
              "Color Selected: " + options[sel]);
      } else if(id.equals("Input")) {
        String val = JOptionPane.showInputDialog(
            "How many fingers do you see?"); 
        txt.setText(val);
      } else if(id.equals("3 Vals")) {
        Object[] selections = {
          "First", "Second", "Third" };
        Object val = JOptionPane.showInputDialog(
          null, "Choose one", "Input",
          JOptionPane.INFORMATION_MESSAGE, 
          null, selections, selections[0]);
        if(val != null)
          txt.setText(
            val.toString());
      }
    }
  };
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i       b[i].addActionListener(al);
      cp.add(b[i]);
    }
    cp.add(txt);
  }
  public static void main(String[] args) {
    Console.run(new MessageBoxes(), 200, 200);
  }
} ///:~

On remarquera que showOptionDialog() et showInputDialog() retournent des objets contenant la valeur entrée par l'utilisateur.

XV-H-14. Menus

Chaque composant capable de contenir un menu, y compris JApplet, JFrame, JDialog et leurs descendants, possède une méthode setJMenuBar() qui prend comme paramètre un JMenuBar (il ne peut y avoir qu'un seul JMenuBar sur un composant donné). On ajoute les JMenus au JMenuBar, et les JMenuItems aux JMenus. On peut attacher un ActionListener à chaque JMenuItem, qui sera lancé quand l'élément de menu est sélectionné.

Contrairement à un système qui utilise des ressources, en Java et Swing il faut assembler à la main tous les menus dans le code source. Voici un exemple très simple de menu :

 
Sélectionnez
//: c13:SimpleMenus.java
// <applet code=SimpleMenus 
// width=200 height=75> </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class SimpleMenus extends JApplet {
  JTextField t = new JTextField(15);
  ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e){
      t.setText(
        ((JMenuItem)e.getSource()).getText());
    }
  };
  JMenu[] menus = { new JMenu("Winken"), 
    new JMenu("Blinken"), new JMenu("Nod") };
  JMenuItem[] items = {
    new JMenuItem("Fee"), new JMenuItem("Fi"),
    new JMenuItem("Fo"),  new JMenuItem("Zip"),
    new JMenuItem("Zap"), new JMenuItem("Zot"), 
    new JMenuItem("Olly"), new JMenuItem("Oxen"),
    new JMenuItem("Free") };
  public void init() {
    for(int i = 0; i       items[i].addActionListener(al);
      menus[i%3].add(items[i]);
    }
    JMenuBar mb = new JMenuBar();
    for(int i = 0; i       mb.add(menus[i]);
    setJMenuBar(mb);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t); 
  }
  public static void main(String[] args) {
    Console.run(new SimpleMenus(), 200, 75);
  }
} ///:~

L'utilisation de l'opérateur modulo [modulus] dans i%3 distribue les éléments de menus parmi les trois JMenus. Chaque JMenuItem doit avoir un ActionListener attaché ; ici le même ActionListener est utilisé partout, mais on en aura besoin normalement d'un pour chaque JMenuItem.

JMenuItem hérite d'AbstractButton, et il a donc certains comportements des boutons. En lui-même, il fournit un élément qui peut être placé dans un menu déroulant. Il y a aussi trois types qui héritent de JMenuItem : JMenu pour contenir d'autres JMenuItems (pour réaliser des menus en cascade), JCheckBoxMenuItem, qui fournit un marquage pour indiquer si l'élément de menu est sélectionné ou pas, et JRadioButtonMenuItem, qui contient un bouton radio.

En tant qu'exemple plus sophistiqué, voici à nouveau les parfums de crèmes glacées, utilisés pour créer des menus. Cet exemple montre également des menus en cascade, des mnémoniques clavier, des JCheckBoxMenuItems, et la façon de changer ces menus dynamiquement :

 
Sélectionnez
//: c13:Menus.java
// Sous-menus, éléments de menu avec boîtes à cocher, permutations de  menus,
// mnémoniques (raccourcis) et commandes d'actions.
// <applet code=Menus width=300
// height=100> </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Menus extends JApplet {
  String[] flavors = { "Chocolate", "Strawberry",
    "Vanilla Fudge Swirl", "Mint Chip", 
    "Mocha Almond Fudge", "Rum Raisin", 
    "Praline Cream", "Mud Pie" };
  JTextField t = new JTextField("No flavor", 30);
  JMenuBar mb1 = new JMenuBar();
  JMenu 
    f = new JMenu("File"),
    m = new JMenu("Flavors"),
    s = new JMenu("Safety");
  // Approche alternative :
  JCheckBoxMenuItem[] safety = {
    new JCheckBoxMenuItem("Guard"),
    new JCheckBoxMenuItem("Hide")
  };
  JMenuItem[] file = {
    new JMenuItem("Open"),
  };
  // Une seconde barre de menu pour échanger :
  JMenuBar mb2 = new JMenuBar();
  JMenu fooBar = new JMenu("fooBar");
  JMenuItem[] other = {
    // Ajouter un raccourci de menu (mnémonique) est très 
    // simple, mais seuls les JMenuItems peuvent les avoir 
    // dans leurs constructeurs:
    new JMenuItem("Foo", KeyEvent.VK_F),
    new JMenuItem("Bar", KeyEvent.VK_A),
    // Pas de raccourci :
    new JMenuItem("Baz"),
  };
  JButton b = new JButton("Swap Menus");
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuBar m = getJMenuBar();
      setJMenuBar(m == mb1 ? mb2 : mb1);
      validate(); // Rafraîchissement de la fenêtre
    }
  }
  class ML implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuItem target = (JMenuItem)e.getSource();
      String actionCommand = 
        target.getActionCommand();
      if(actionCommand.equals("Open")) {
        String s = t.getText();
        boolean chosen = false;
        for(int i = 0; i           if(s.equals(flavors[i])) chosen = true;
        if(!chosen)
          t.setText("Choose a flavor first!");
        else
          t.setText("Opening "+ s +". Mmm, mm!");
      }
    }
  }
  class FL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuItem target = (JMenuItem)e.getSource();
      t.setText(target.getText());
    }
  }
  // Alternativement, on peut créer une classe
// différente pour chaque JMenuItem. Ensuite
// il n'est plus nécessaire de rechercher de laquelle il s'agit :
  class FooL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Foo selected");
    }
  }
  class BarL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Bar selected");
    }
  }
  class BazL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Baz selected");
    }
  }
  class CMIL implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      JCheckBoxMenuItem target = 
        (JCheckBoxMenuItem)e.getSource();
      String actionCommand = 
        target.getActionCommand();
      if(actionCommand.equals("Guard"))
        t.setText("Guard the Ice Cream! " +
          "Guarding is " + target.getState());
      else if(actionCommand.equals("Hide"))
        t.setText("Hide the Ice Cream! " +
          "Is it cold? " + target.getState());
    }
  }
  public void init() {
    ML ml = new ML();
    CMIL cmil = new CMIL();
    safety[0].setActionCommand("Guard");
    safety[0].setMnemonic(KeyEvent.VK_G);
    safety[0].addItemListener(cmil);
    safety[1].setActionCommand("Hide");
    safety[0].setMnemonic(KeyEvent.VK_H);
    safety[1].addItemListener(cmil);
    other[0].addActionListener(new FooL());
    other[1].addActionListener(new BarL());
    other[2].addActionListener(new BazL());
    FL fl = new FL();
    for(int i = 0; i       JMenuItem mi = new JMenuItem(flavors[i]);
      mi.addActionListener(fl);
      m.add(mi);
      // Ajout de séparateurs par  intervalles:
      if((i+1) % 3 == 0) 
        m.addSeparator();
    }
    for(int i = 0; i       s.add(safety[i]);
    s.setMnemonic(KeyEvent.VK_A);
    f.add(s);
    f.setMnemonic(KeyEvent.VK_F);
    for(int i = 0; i       file[i].addActionListener(fl);
      f.add(file[i]);
    }
    mb1.add(f);
    mb1.add(m);
    setJMenuBar(mb1);
    t.setEditable(false);
    Container cp = getContentPane();
    cp.add(t, BorderLayout.CENTER);
    // Installation du système d'échange de menus:
    b.addActionListener(new BL());
    b.setMnemonic(KeyEvent.VK_S);
    cp.add(b, BorderLayout.NORTH);
    for(int i = 0; i       fooBar.add(other[i]);
    fooBar.setMnemonic(KeyEvent.VK_B);
    mb2.add(fooBar);
  }
  public static void main(String[] args) {
    Console.run(new Menus(), 300, 100);
  }
} ///:~

Dans ce programme, j'ai placé les éléments de menus dans des tableaux et ensuite j'ai parcouru chaque tableau en appelant add() pour chaque JMenuItem. Ceci rend l'ajout ou la suppression d'un élément de menu un peu moins fastidieux.

Ce programme crée deux JMenuBars pour démontrer que les barres de menu peuvent être échangées dynamiquement à l'exécution du programme. On peut voir qu'un JMenuBar est composé de JMenus, et que chaque JMenu est composé de JMenuItems, JCheckBoxMenuItems, ou même d'autres JMenus (qui produisent des sous-menus). Une fois construit un JMenuBar, il peut être installé dans le programme courant avec la méthode setJMenuBar(). Remarquons que lorsque le bouton est cliqué, il regarde quel menu est installé en appelant getJMenuBar(), et à ce moment il le remplace par l'autre barre de menu.

Lorsqu'on teste "Open", il faut remarquer que l'orthographe et les majuscules/minuscules sont cruciaux, et que Java ne signale pas d'erreur s'il n'y a pas correspondance avec "Open". Ce genre de comparaison de chaînes est une source d'erreurs de programmation.

Le cochage et le décochage des éléments de menus sont pris en compte automatiquement. Le code gérant les JCheckBoxMenuItems montre deux façons différentes de déterminer ce qui a été coché : la correspondance des chaînes (qui, comme mentionné ci-dessus, n'est pas une approche très sûre bien qu'on la rencontre) et la correspondance de l'objet cible de l'événement. Il montre aussi que la méthode getState() peut être utilisée pour connaître son état. On peut également changer l'état d'un JCheckBoxMenuItem à l'aide de setState().

Les événements des menus sont un peu inconsistants et peuvent prêter à confusion : les JMenuItems utilisent des ActionListeners, mais les JCheckboxMenuItems utilisent des ItemListeners. Les objets JMenu peuvent aussi supporter des ActionListeners, mais ce n'est généralement pas très utile. En général, on attache des listeners à chaque JMenuItem, JCheckBoxMenuItem, ou JRadioButtonMenuItem, mais l'exemple montre des ItemListeners et des ActionListeners attachés aux divers composants de menus.

Swing supporte les mnémoniques, ou raccourcis clavier, de sorte qu'on peut sélectionner tout ce qui est dérivé de AbstractButton (bouton, menu, élément, etc.) en utilisant le clavier à la place de la souris. C'est assez simple : pour le JMenuItem on peut utiliser le constructeur surchargé qui prend en deuxième argument l'identificateur de la touche. Cependant, la plupart des AbstractButtons n'ont pas ce constructeur ; une manière plus générale de résoudre ce problème est d'utiliser la méthode setMnemonic(). L'exemple ci-dessus ajoute une mnémonique au bouton et à certains des éléments de menus : les indicateurs de raccourcis apparaissent automatiquement sur les composants.

On peut aussi voir l'utilisation de setActionCommand(). Ceci paraît un peu bizarre, car dans chaque cas la commande d'action [action command] est exactement la même que le label sur le composant du menu. Pourquoi ne pas utiliser simplement le label plutôt que cette chaîne de remplacement ? Le problème est l'internationalisation. Si on réoriente ce programme vers une autre langue, on désire changer uniquement le label du menu, et pas le code (ce qui introduirait à coup sûr d'autres erreurs). Donc pour faciliter ceci pour les codes qui testent la chaîne associée à un composant de menu, la commande d'action peut être invariante tandis que le label du menu peut changer. Tout le code fonctionne avec la commande d'action, de sorte qu'il n'est pas touché par les modifications des labels des menus. Remarquons que dans ce programme on ne recherche pas des commandes d'actions pour tous les composants de menus, de sorte que ceux qui ne sont pas examinés n'ont pas de commande d'action positionnée.

La plus grosse partie du travail se trouve dans les listeners. BL effectue l'échange des JMenuBars. Dans ML, l'approche du "qui a sonné ?" est utilisée en utilisant la source de l'ActionEvent et en l'émettant vers un JMenuItem, en faisant passer la chaîne de la commande d'action à travers une instruction if en cascade.

Le listener FL est simple, bien qu'il gère les différents parfums du menu parfums. Cette approche est utile si la logique est simple, mais en général, on utilisera l'approche de FooL, BarL et BazL, dans lesquels ils sont chacun attachés à un seul composant de menu de sorte qu'il n'est pas nécessaire d'avoir une logique de détection supplémentaire, et on sait exactement qui a appelé le listener. Même avec la profusion de classes générées de cette façon, le code interne tend à être plus petit et le traitement est plus fiable.

On peut voir que le code d'un menu devient rapidement long et désordonné. C'est un autre cas où l'utilisation d'un GUI builder est la solution appropriée. Un bon outil gérera également la maintenance des menus.

XV-H-15. Menus pop-up

La façon la plus directe d'implémenter un JPopupMenu est de créer une classe interne qui étend MouseAdapter, puis d'ajouter un objet de cette classe interne à chaque composant pour lequel on veut créer le pop-up :

 
Sélectionnez
//: c13:Popup.java
// Création de menus popup avec Swing.
// <applet code=Popup
//  width=300 height=200></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Popup extends JApplet {
  JPopupMenu popup = new JPopupMenu();
  JTextField t = new JTextField(10);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e){
        t.setText(
          ((JMenuItem)e.getSource()).getText());
      }
    };
    JMenuItem m = new JMenuItem("Hither");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Yon");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Afar");
    m.addActionListener(al);
    popup.add(m);
    popup.addSeparator();
    m = new JMenuItem("Stay Here");
    m.addActionListener(al);
    popup.add(m);
    PopupListener pl = new PopupListener();
    addMouseListener(pl);
    t.addMouseListener(pl);
  }
  class PopupListener extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      maybeShowPopup(e);
    }
    public void mouseReleased(MouseEvent e) {
      maybeShowPopup(e);
    }
    private void maybeShowPopup(MouseEvent e) {
      if(e.isPopupTrigger()) {
        popup.show(
          e.getComponent(), e.getX(), e.getY());
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new Popup(), 300, 200);
  }
} ///:~

Le même ActionListener est ajouté à chaque JMenuItem de façon à prendre le texte du label du menu et l'insérer dans le JTextField.

XV-H-16. Dessiner

Dans un bon outil de GUI, dessiner devrait être assez facile, et ça l'est dans la bibliothèque Swing. Le problème de tout exemple de dessin est que les calculs qui déterminent où vont les éléments sont souvent beaucoup plus compliqués que les appels aux sous-programmes de dessin, et que ces calculs sont souvent mélangés aux appels de dessin, de sorte que l'interface semble plus compliquée qu'elle ne l'est.

Pour simplifier, considérons le problème de la représentation de données sur l'écran. Ici, les données sont fournies par la méthode intrinsèque Math.sin() qui est la fonction mathématique sinus. Pour rendre les choses un peu plus intéressantes, et pour mieux montrer combien il est facile d'utiliser les composants Swing, un curseur sera placé en bas du formulaire pour contrôler dynamiquement le nombre de cycles du sinus affiché. De plus, si on redimensionne la fenêtre, on verra le sinus s'adapter de lui-même à la nouvelle taille de la fenêtre.

Dans l'exemple suivant, toute l'intelligence concernant le dessin est contenue dans la classe SineDraw; la classe SineWave configure simplement le programme et le curseur. Dans SineDraw, la méthode setCycles() fournit un moyen pour permettre à un autre objet (le curseur dans ce cas) de contrôler le nombre de cycles.

 
Sélectionnez
//: c13:SineWave.java
// Dessin avec Swing, en utilisant un JSlider.
// <applet code=SineWave
//  width=700 height=400></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

class SineDraw extends JPanel {
  static final int SCALEFACTOR = 200;
  int cycles;
  int points;
  double[] sines;
  int[] pts;
  SineDraw() { setCycles(5); }
  public void setCycles(int newCycles) {
    cycles = newCycles;
    points = SCALEFACTOR * cycles * 2;
    sines = new double[points];
    pts = new int[points];
    for(int i = 0; i       double radians = (Math.PI/SCALEFACTOR) * i;
      sines[i] = Math.sin(radians);
    }
    repaint();
  }    
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    int maxWidth = getWidth();
    double hstep = (double)maxWidth/(double)points;
    int maxHeight = getHeight();
    for(int i = 0; i       pts[i] = (int)(sines[i] * maxHeight/2 * .95
                     + maxHeight/2);
    g.setColor(Color.red);
    for(int i = 1; i       int x1 = (int)((i - 1) * hstep);
      int x2 = (int)(i * hstep);
      int y1 = pts[i-1];
      int y2 = pts[i];
      g.drawLine(x1, y1, x2, y2);
    }
  }
}

public class SineWave extends JApplet {
  SineDraw sines = new SineDraw();
  JSlider cycles = new JSlider(1, 30, 5);
  public void init() {
    Container cp = getContentPane();
    cp.add(sines);
    cycles.addChangeListener(new ChangeListener(){
      public void stateChanged(ChangeEvent e) {
        sines.setCycles(
          ((JSlider)e.getSource()).getValue());
      }
    });
    cp.add(BorderLayout.SOUTH, cycles);
  }
  public static void main(String[] args) {
    Console.run(new SineWave(), 700, 400);
  }
} ///:~

Tous les membres de données et tableaux sont utilisés dans le calcul des points du sinus : cycles indique le nombre de périodes complètes de sinus désiré, points contient le nombre total de points qui seront tracés, sines contient les valeurs de la fonction sinus, et pts contient les coordonnées y des points qui seront tracés sur le JPanel. La méthode setCycles() construit le tableau selon le nombre de points nécessaires et remplit le tableau sines de valeurs. En appelant repaint(), setCycles force l'appel de paintComponent() , afin que le reste des calculs et le dessin aient lieu.

La première chose à faire lorsqu'on redéfinit paintComponent() est d'appeler la version de base de la méthode. Ensuite on peut faire ce que l'on veut ; normalement cela signifie utiliser les méthodes de Graphics qu'on peut trouver dans la documentation de java.awt.Graphics (dans la documentation HTML de java.sun.com) pour dessiner et peindre des pixels sur le JPanel. On peut voir ici que la plupart du code concerne l'exécution des calculs, les deux seules méthodes qui manipulent effectivement l'écran sont setColor() et drawLine(). Vous aurez probablement la même sensation lorsque vous créerez votre propre programme d'affichage de données graphiques : vous passerez la plus grande partie du temps à déterminer ce qu'il faut dessiner, mais le dessin en lui-même sera assez simple.

Lorsque j'ai créé ce programme, j'ai passé le plus gros de mon temps à obtenir la courbe du sinus à afficher. Ceci fait, j'ai pensé que ce serait bien de pouvoir modifier dynamiquement le nombre de cycles. Mes expériences de programmation de ce genre de choses dans d'autres langages me rendaient un peu réticent, mais cette partie se révéla la partie la plus facile du projet. J'ai créé un JSlider (les arguments sont respectivement la valeur de gauche du JSlider, la valeur de droite, et la valeur initiale, mais il existe d'autres constructeurs) et je l'ai déposé dans le JApplet. Ensuite j'ai regardé dans la documentation HTML et j'ai vu que le seul listener était le addChangeListener, qui était déclenché chaque fois que le curseur était déplacé suffisamment pour produire une nouvelle valeur. La seule méthode pour cela était évidemment appelée stateChanged(), qui fournit un objet ChangeEvent, de manière à pouvoir rechercher la source de la modification et obtenir la nouvelle valeur. En appelant setCycles() des objets sines, la nouvelle valeur est prise en compte et le JPanel est redessiné.

En général, on verra que la plupart des problèmes Swing peuvent être résolus en suivant un processus semblable, et on verra qu'il est en général assez simple, même si on n'a pas utilisé auparavant un composant donné.

Si le problème est plus compliqué, il y a d'autres solutions plus sophistiquées, par exemple les composants JavaBeans de fournisseurs tiers, et l'API Java 2D. Ces solutions sortent du cadre de ce livre, mais vous devriez les prendre en considération si votre code de dessin devient trop coûteux.

XV-H-17. Boîtes de dialogue

Une boîte de dialogue est une fenêtre qui est issue d'une autre fenêtre. Son but est de traiter un problème spécifique sans encombrer la fenêtre d'origine avec ces détails. Les boîtes de dialogue sont fortement utilisées dans les environnements de programmation à fenêtres, mais moins fréquemment utilisées dans les applets.

Pour créer une boîte de dialogue, il faut hériter de JDialog, qui est simplement une sorte de Window, comme les JFrames. Un JDialog possède un layout manager (qui est par défaut le BorderLayout) auquel on ajoute des listeners d'événements pour traiter ceux-ci. Il y a une différence importante : on ne veut pas fermer l'application lors de l'appel de windowClosing(). Au lieu de cela, on libère les ressources utilisées par la fenêtre de dialogue en appelant dispose(). Voici un exemple très simple :

 
Sélectionnez
//: c13:Dialogs.java
// Création et utilisation de boîtes de dialogue.
// <applet code=Dialogs width=125 height=75>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

class MyDialog extends JDialog {
  public MyDialog(JFrame parent) {
    super(parent, "My dialog", true);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(new JLabel("Here is my dialog"));
    JButton ok = new JButton("OK");
    ok.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        dispose(); // Ferme le dialogue
      }
    });
    cp.add(ok);
    setSize(150,125);
  }
}

public class Dialogs extends JApplet {
  JButton b1 = new JButton("Dialog Box");
  MyDialog dlg = new MyDialog(null);
  public void init() {
    b1.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        dlg.show();
      }
    });
    getContentPane().add(b1);
  }
  public static void main(String[] args) {
    Console.run(new Dialogs(), 125, 75);
  }
} ///:~

Une fois le JDialog créé, la méthode show() doit être appelée pour l'afficher et l'activer. Pour que le dialogue se ferme, il faut appeler dispose().

On remarquera que tout ce qui sort d'une applet, y compris les boîtes de dialogue, n'est pas digne de confiance. C'est-à-dire qu'on obtient un avertissement dans la fenêtre qui apparaît. Ceci est dû au fait qu'en théorie il serait possible de tromper l'utilisateur et lui faire croire qu'il a à faire avec une de ses applications normales et de le faire taper son numéro de carte de crédit, qui partirait alors sur le Web. Une applet est toujours attachée à une page Web et visible dans un navigateur, tandis qu'une boîte de dialogue est détachée, et tout ceci est donc possible en théorie. Le résultat est qu'il n'est pas fréquent de voir une applet qui utilise une boîte de dialogue.

L'exemple suivant est plus complexe; la boîte de dialogue est composée d'une grille (en utilisant GridLayout) d'un type de bouton particulier qui est défini ici comme la classe ToeButton. Ce bouton dessine un cadre autour de lui et, selon son état, un blanc, un x ou un o au milieu. Il démarre en blanc, et ensuite, selon à qui c'est le tour, se modifie en x ou en o. Cependant, il transformera le x en o et vice versa lorsqu'on clique sur le bouton (ceci rend le principe du tic-tac-toe seulement un peu plus ennuyeux qu'il ne l'est déjà). De plus, la boîte de dialogue peut être définie avec un nombre quelconque de rangées et de colonnes dans la fenêtre principale de l'application.

 
Sélectionnez
//: c13:TicTacToe.java
// Démonstration de boîtes de dialogue
// et création de vos propres composants.
// <applet code=TicTacToe
//  width=200 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class TicTacToe extends JApplet {
  JTextField 
    rows = new JTextField("3"),
    cols = new JTextField("3");
  static final int BLANK = 0, XX = 1, OO = 2;
  class ToeDialog extends JDialog {
    int turn = XX; // Démarre avec x a jouer
    // w = nombre de cellules en largeur
    // h = nombre de cellules en hauteur
    public ToeDialog(int w, int h) {
      setTitle("The game itself");
      Container cp = getContentPane();
      cp.setLayout(new GridLayout(w, h));
      for(int i = 0; i         cp.add(new ToeButton());
      setSize(w * 50, h * 50);
      // fermeture du dialogue en JDK 1.3 :
      //#setDefaultCloseOperation(
      //#  DISPOSE_ON_CLOSE);
      // fermeture du dialogue en JDK 1.2 :
      addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e){
          dispose();
        }
      });    
    }
    class ToeButton extends JPanel {
      int state = BLANK;
      public ToeButton() {
        addMouseListener(new ML());
      }
      public void paintComponent(Graphics g) {
        super.paintComponent(g);
        int x1 = 0;
        int y1 = 0;
        int x2 = getSize().width - 1;
        int y2 = getSize().height - 1;
        g.drawRect(x1, y1, x2, y2);
        x1 = x2/4;
        y1 = y2/4;
        int wide = x2/2;
        int high = y2/2;
        if(state == XX) {
          g.drawLine(x1, y1, 
            x1 + wide, y1 + high);
          g.drawLine(x1, y1 + high, 
            x1 + wide, y1);
        }
        if(state == OO) {
          g.drawOval(x1, y1, 
            x1 + wide/2, y1 + high/2);
        }
      }
      class ML extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
          if(state == BLANK) {
            state = turn;
            turn = (turn == XX ? OO : XX);
          } 
          else
            state = (state == XX ? OO : XX);
          repaint();
        }
      }
    }
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JDialog d = new ToeDialog(
        Integer.parseInt(rows.getText()),
        Integer.parseInt(cols.getText()));
      d.setVisible(true);
    }
  }
  public void init() {
    JPanel p = new JPanel();
    p.setLayout(new GridLayout(2,2));
    p.add(new JLabel("Rows", JLabel.CENTER));
    p.add(rows);
    p.add(new JLabel("Columns", JLabel.CENTER));
    p.add(cols);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.NORTH);
    JButton b = new JButton("go");
    b.addActionListener(new BL());
    cp.add(b, BorderLayout.SOUTH);
  }
  public static void main(String[] args) {
    Console.run(new TicTacToe(), 200, 100);
  }
} ///:~

Comme les statics peuvent être uniquement au niveau le plus extérieur de la classe, les classes internes ne peuvent pas avoir de données static ni de classes internes static.

La méthode paintComponent() dessine le carré autour du panneau, et le x ou le o. C'est rempli de calculs fastidieux, mais c'est direct.

Les clics de souris sont capturés par le MouseListener, qui vérifie d'abord si le panneau a déjà quelque chose d'écrit sur lui. Si ce n'est pas le cas, on recherche la fenêtre parente pour déterminer à qui est le tour, et on positionne l'état du ToeButton en conséquence. Le ToeButton retrouve le parent par le mécanisme de la classe interne, et passe au tour suivant. Si le bouton affiche déjà un x ou un o, son affichage est inversé. On peut voir dans ces calculs l'usage pratique du if-else ternaire décrit au titre V. On repeint le ToeButton chaque fois qu'il change d'état.

Le constructeur de ToeDialog est assez simple : il ajoute à un GridLayout autant de boutons que demandé, puis redimensionne chaque bouton à 50 pixels.

TicTacToe installe l'ensemble de l'application par la création de JTextFields (pour entrer le nombre de rangées et colonnes de la grille de boutons) et le bouton « go » avec son ActionListener. Lorsqu'on appuie sur le bouton, les données dans les JTextFields doivent être récupérées et, puisqu'elles sont au format String, transformées en ints en utilisant la méthode statique Integer.parseInt().

XV-H-18. Dialogues pour les fichiers [File dialogs]

Certains systèmes d'exploitation ont des boîtes de dialogue standard pour gérer certaines choses telles que les fontes, les couleurs, les imprimantes, etc. En tout cas, pratiquement tous les systèmes d'exploitation graphiques fournissent les moyens d'ouvrir et de sauver les fichiers, et le JFileChooser de Java les encapsule pour une utilisation facile.

L'application suivante utilise deux sortes de dialogues JFileChooser, un pour l'ouverture et un pour la sauvegarde. La plupart du code devrait maintenant être familier, et toute la partie intéressante se trouve dans les action listeners pour les différents clics de boutons :

 
Sélectionnez
//: c13:FileChooserTest.java
// Démonstration de boîtes de dialogues de fichiers.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class FileChooserTest extends JFrame {
  JTextField 
    filename = new JTextField(),
    dir = new JTextField();
  JButton 
    open = new JButton("Open"),
    save = new JButton("Save");
  public FileChooserTest() {
    JPanel p = new JPanel();
    open.addActionListener(new OpenL());
    p.add(open);
    save.addActionListener(new SaveL());
    p.add(save);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.SOUTH);
    dir.setEditable(false);
    filename.setEditable(false);
    p = new JPanel();
    p.setLayout(new GridLayout(2,1));
    p.add(filename);
    p.add(dir);
    cp.add(p, BorderLayout.NORTH);
  }
  class OpenL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser();
      // Démontre le dialogue "Open" :
      int rVal = 
        c.showOpenDialog(FileChooserTest.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        filename.setText(
          c.getSelectedFile().getName());
          dir.setText(
            c.getCurrentDirectory().toString());
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      }
    }
  }
  class SaveL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser();
      // Démontre le dialogue "Save" :
      int rVal = 
        c.showSaveDialog(FileChooserTest.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        filename.setText(
          c.getSelectedFile().getName());
          dir.setText(
            c.getCurrentDirectory().toString());
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new FileChooserTest(), 250, 110);
  }
} ///:~

Pour un dialogue d'ouverture de fichier, on appelle showOpenDialog(), et pour un dialogue de sauvegarde de fichier, on appelle showSaveDialog(). Ces commandes ne reviennent que lorsque le dialogue est fermé. L'objet JFileChooser existe encore, de sorte qu'on peut en lire les données. Les méthodes getSelectedFile() et getCurrentDirectory() sont deux façons d'obtenir les résultats de l'opération. Si celles-ci renvoient null, cela signifie que l'utilisateur a abandonné le dialogue.

XV-H-19. HTML sur des composants Swing

Tout composant acceptant du texte peut également accepter du texte HTML, qu'il reformatera selon les règles HTML. Ceci signifie qu'on peut très facilement ajouter du texte de fantaisie à un composant Swing. Par exemple :

 
Sélectionnez
//: c13:HTMLButton.java
// Mettre du texte HTML sur des composants Swing.
// <applet code=HTMLButton width=200 height=500>
// </applet>
importjavax.swing.*;
importjava.awt.event.*;
importjava.awt.*;
importcom.bruceeckel.swing.*;

publicclassHTMLButton extendsJApplet {
JButton b = newJButton("<html><b><font size=+2>"+
   "<center>Hello!<br><i>Press me now!");
publicvoidinit() {
   b.addActionListener(newActionListener() {
     publicvoidactionPerformed(ActionEvent e){
       getContentPane().add(newJLabel("<html>"+
         "<i><font size=+4>Kapow!"));
       // Force un réalignement pour
       // inclure le nouveau label:
       validate();
     }
   });
   Container cp = getContentPane();
   cp.setLayout(newFlowLayout());
   cp.add(b);
}
publicstaticvoidmain(String[] args) {
   Console.run(newHTMLButton(), 200, 500);
}
} ///:~

Le texte doit commencer avec, et ensuite on peut utiliser les tags HTML normaux. Remarquons que les tags de fermeture ne sont pas obligatoires.

L'ActionListener ajoute au formulaire un nouveau JLabel contenant du texte HTML. Comme ce label n'est pas ajouté dans la méthode init(), on doit appeler la méthode validate() du conteneur de façon à forcer une redisposition des composants (et de ce fait un affichage du nouveau label).

On peut également ajouter du texte HTML à un JTabbedPane, JMenuItem, JToolTip, JRadioButton ou un JCheckBox.

XV-H-20. Curseurs [sliders] et barres de progression [progress bars]

Un slider (qu'on a déjà utilisé dans l'exemple du sinus) permet à l'utilisateur de rentrer une donnée en déplaçant un point en avant ou en arrière, ce qui est intuitif dans certains cas (un contrôle de volume, par exemple). Un progress bar représente une donnée en remplissant proportionnellement un espace vide pour que l'utilisateur ait une idée de la valeur. Mon exemple favori pour ces composants consiste à simplement lier le curseur à la barre de progression, de sorte que lorsqu'on déplace le curseur la barre de progression change en conséquence :

 
Sélectionnez
//: c13:Progress.java
// Utilisation de progress bars et de sliders.
// <applet code=Progress
//  width=300 height=200></applet>
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjavax.swing.event.*;
importjavax.swing.border.*;
importcom.bruceeckel.swing.*;

publicclassProgress extendsJApplet {
JProgressBar pb = newJProgressBar();
JSlider sb = 
   newJSlider(JSlider.HORIZONTAL, 0, 100, 60);
publicvoidinit() {
   Container cp = getContentPane();
   cp.setLayout(newGridLayout(2,1));
   cp.add(pb);
   sb.setValue(0);
   sb.setPaintTicks(true);
   sb.setMajorTickSpacing(20);
   sb.setMinorTickSpacing(5);
   sb.setBorder(newTitledBorder("Slide Me"));
   pb.setModel(sb.getModel()); // Partage du modèle
   cp.add(sb);
}
publicstaticvoidmain(String[] args) {
   Console.run(newProgress(), 300, 200);
}
} ///:~

La clé du lien entre les deux composants se trouve dans le partage de leur modèle, dans la ligne :

 
Sélectionnez
pb.setModel(sb.getModel());

Naturellement, on pourrait aussi contrôler les deux composants en utilisant un listener, mais ceci est plus direct pour les cas simples.

Le JProgressBar est assez simple, mais le JSlider a un grand nombre d'options, telles que l'orientation et les graduations mineures et majeures. Remarquons la simplicité de l'ajout d'une bordure avec titre.

XV-H-21. Arbres [Trees]

L'utilisation d'un JTree peut être aussi simple que ceci :

 
Sélectionnez
add(newJTree(
newObject[] {"this", "that", "other"}));

Ceci affiche un arbre rudimentaire. L'API pour les arbres est vaste, probablement une des plus importantes de Swing. On peut faire à peu près tout ce qu'on veut avec des arbres, mais les tâches plus complexes demandent davantage de recherches et d'expérimentations.

Heureusement, il y a un niveau intermédiaire fourni dans la bibliothèque : les composants arbres par défaut, qui font en général ce dont on a besoin, de sorte que la plupart du temps on peut se contenter de ces composants, et ce n'est que dans des cas particuliers qu'on aura besoin d'approfondir et de comprendre plus en détail les arbres.

L'exemple suivant utilise les composants arbres par défaut pour afficher un arbre dans une applet. Lorsqu'on appuie sur le bouton, un nouveau sous-arbre est ajouté sous le nœud sélectionné (si aucun nœud n'est sélectionné, le nœud racine est utilisé) :

 
Sélectionnez
//: c13:Trees.java
// Exemple d'arbre Swing simple. Les arbres peuvent
// être beaucoup plus complexes que celui-ci.
// <applet code=Trees
//  width=250 height=250></applet>
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjavax.swing.tree.*;
importcom.bruceeckel.swing.*;

// Prend un tableau de Strings et crée un nœud à partir
// du premier élément, et des feuilles avec les autres :
classBranch {
DefaultMutableTreeNode r;
publicBranch(String[] data) {
   r = newDefaultMutableTreeNode(data[0]);
   for(inti = 1; i      r.add(newDefaultMutableTreeNode(data[i]));
}
publicDefaultMutableTreeNode node() { 
   returnr; 
}
}  

publicclassTrees extendsJApplet {
String[][] data = {
   { "Colors", "Red", "Blue", "Green"},
   { "Flavors", "Tart", "Sweet", "Bland"},
   { "Length", "Short", "Medium", "Long"},
   { "Volume", "High", "Medium", "Low"},
   { "Temperature", "High", "Medium", "Low"},
   { "Intensity", "High", "Medium", "Low"},
};
staticinti = 0;
DefaultMutableTreeNode root, child, chosen;
JTree tree;
DefaultTreeModel model;
publicvoidinit() {
   Container cp = getContentPane();
   root = newDefaultMutableTreeNode("root");
   tree = newJTree(root);
   // On l'ajoute et on le rend scrollable :
   cp.add(newJScrollPane(tree), 
     BorderLayout.CENTER);
   // Obtention du modèle de l'arbre :
   model =(DefaultTreeModel)tree.getModel();
   JButton test = newJButton("Press me");
   test.addActionListener(newActionListener() {
     publicvoidactionPerformed(ActionEvent e){
       if(i          child = newBranch(data[i++]).node();
         // Quel est le dernier élément cliqué ?
         chosen = (DefaultMutableTreeNode)
           tree.getLastSelectedPathComponent();
         if(chosen ==null) chosen = root;
         // Le modèle créera l'événement approprié.
         // En réponse, l'arbre se remettra à jour :
         model.insertNodeInto(child, chosen, 0);
         // Ceci place le nouveau nœud
         // sur le nœud actuellement sélectionné.
       }
     }
   });
   // Change les couleurs des boutons :
   test.setBackground(Color.blue);
   test.setForeground(Color.white);
   JPanel p = newJPanel();
   p.add(test);
   cp.add(p, BorderLayout.SOUTH);
}
publicstaticvoidmain(String[] args) {
   Console.run(newTrees(), 250, 250);
}
} ///:~

La première classe, Branch, est un outil qui prend un tableau et construit un DefaultMutableTreeNodeavec la première String comme racine, et le reste des Strings du tableau pour les feuilles. Ensuite node() peut être appelé pour créer la racine de cette branche.

La classe Trees contient un tableau de Strings à deux dimensions à partir duquel des Branches peuvent être créées, ainsi qu'un static int i pour servir d'index à travers ce tableau. L'objet DefaultMutableTreeNode contient les nœuds, mais la représentation physique à l'écran est contrôlée par le JTree et son modèle associé, le DefaultTreeModel. Notons que lorsque le JTree est ajouté à l'applet, il est encapsulé dans un JScrollPane : c'est suffisant pour permettre un scrolling automatique.

Le JTree est contrôlé par son modèle. Lorsqu'on modifie les données du modèle, celui-ci génère un événement qui force le JTree à effectuer les mises à jour nécessaires de la partie visible de la représentation de l'arbre. Dans init(), le modèle est obtenu par appel à getModel(). Lorsqu'on appuie sur le bouton, une nouvelle branche est créée. Ensuite le composant actuellement sélectionné est recherché (on utilise la racine si rien n'est sélectionné) et la méthode insertNodeInto() du modèle effectue la modification de l'arbre et provoque sa mise à jour.

Un exemple comme ci-dessus peut vous donner ce dont vous avez besoin pour utiliser un arbre. Cependant les arbres ont la possibilité de faire à peu près tout ce qui est imaginable ; chaque fois que le mot default apparaît dans l'exemple ci-dessus, on peut y substituer sa propre classe pour obtenir un comportement différent. Mais attention : la plupart de ces classes ont une interface importante, de sorte qu'on risque de passer du temps à comprendre la complexité des arbres. Cependant, on a affaire ici à une bonne conception, et les autres solutions sont en général bien moins bonnes.

XV-H-22. Tables

Comme les arbres, les tables en Swing sont vastes et puissantes. Elles sont destinées principalement à être la populaire grille d'interface avec les bases de données via la Connectivité Bases de Données Java : Java DataBase Connectivity (JDBC, présenté dans le titre XVII) et pour cela elles ont une flexibilité énorme, que l'on paie en complexité. Il y a ici suffisamment pour servir de base à un tableur complet et pourrait probablement être le sujet d'un livre complet. Cependant, il est également possible de créer une name="Index1768">JTable relativement simple si on en comprend les bases.

La JTable contrôle la façon dont les données sont affichées, tandis que le TableModel contrôle les données elles-mêmes. Donc pour créer une JTable on créera d'abord un TableModel. On peut implémenter complètement l'interface TableModel, mais il est souvent plus simple d'hériter de la classe utilitaire AbstractTableModel :

 
Sélectionnez
//: c13:Table.java
// Démonstration simple d'une JTable.
// <applet code=Table
//  width=350 height=200></applet>
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjavax.swing.table.*;
importjavax.swing.event.*;
importcom.bruceeckel.swing.*;

publicclassTable extendsJApplet {
JTextArea txt = newJTextArea(4, 20);
// Le TableModel contrôle toutes les données :
classDataModel extendsAbstractTableModel {
   Object[][] data = {
     {"one", "two", "three", "four"},
     {"five", "six", "seven", "eight"},
     {"nine", "ten", "eleven", "twelve"},
   };
   // Imprime les données lorsque la table change :
   classTML implementsTableModelListener {
     publicvoidtableChanged(TableModelEvent e){
       txt.setText(""); // Vider le texte
       for(inti = 0; i          for(intj = 0; j            txt.append(data[i][j] + " ");
         txt.append("\n");
       }
     }
   }
   publicDataModel() {
     addTableModelListener(newTML());
   }
   publicintgetColumnCount() { 
     returndata[0].length; 
   }
   publicintgetRowCount() { 
     returndata.length;
   }
   publicObject getValueAt(introw, intcol) {
     returndata[row][col]; 
   }
   publicvoid
   setValueAt(Object val, introw, intcol) {
     data[row][col] = val;
     // Indique que le changement a eu lieu :
     fireTableDataChanged();
   }
   publicboolean
   isCellEditable(introw, intcol) { 
     returntrue; 
   }
}
publicvoidinit() {
   Container cp = getContentPane();
   JTable table = newJTable(newDataModel());
   cp.add(newJScrollPane(table));
   cp.add(BorderLayout.SOUTH, txt);
}
publicstaticvoidmain(String[] args) {
   Console.run(newTable(), 350, 200);
}
} ///:~

DataModel contient un tableau de données, mais on pourrait aussi obtenir les données depuis une autre source telle qu'une base de données. Le constructeur ajoute un TableModelListener qui imprime le tableau chaque fois que la table est modifiée. Les autres méthodes suivent les conventions de nommage des Beans ; elles sont utilisées par la JTable lorsqu'elle veut présenter les informations contenues dans DataModel. AbstractTableModel fournit des méthodes par défaut pour setValueAt() et isCellEditable() qui interdisent les modifications de données, de sorte que ces méthodes devront être redéfinies si on veut pouvoir modifier les données.

Une fois obtenu un TableModel, il suffit de le passer au constructeur de la JTable. Tous les détails concernant l'affichage, les modifications et la mise à jour seront automatiquement gérés. Cet exemple place également la JTable dans un JScrollPane.

XV-H-23. Sélection de l'aspect de l'interface [Look & Feel]

Un des aspects très intéressants de Swing est le name="Index1770">Pluggable Look & Feel. Il permet à un programme d'émuler le look and feel de divers environnements d'exploitation. On peut même faire toutes sortes de choses comme changer le look and feel pendant l'exécution du programme. Toutefois, en général on désire soit sélectionner le look and feel toutes plateformes (qui est le Metal de Swing), soit sélectionner le look and feel du système courant, de sorte à donner l'impression que le programme Java a été créé spécifiquement pour ce système. Le code permettant de sélectionner chacun de ces comportements est assez simple, mais il faut l'exécuter avant de créer les composants visuels, car ceux-ci seront créés selon le look and feel courant et ne seront pas changés par le simple changement de look and feel au milieu du programme (ce processus est compliqué et peu banal, et nous en laisserons le développement à des livres spécifiques sur Swing).

En fait, si on veut utiliser le look and feel toutes plateformes (metal) qui est la caractéristique des programmes Swing, il n'y a rien de particulier à faire, c'est la valeur par défaut. Si au contraire on veut utiliser le look and feel de l'environnement d'exploitation courant, il suffit d'insérer le code suivant, normalement au début du main(), mais de toute façon avant d'ajouter des composants :

 
Sélectionnez
try{
UIManager.setLookAndFeel(UIManager.
   getSystemLookAndFeelClassName());
} catch(Exception e) {}

Il n'y a pas besoin de faire quoi que ce soit dans la clause catch, car le UIManager se positionnera par défaut au look and feel toutes plateformes si votre tentative d'installation d'un des autres échoue. Toutefois, pour le débogage, l'exception peut être utile, de sorte qu'on peut au minimum placer une instruction d'impression dans la clause catch.

Voici un programme qui utilise un argument de ligne de commande pour sélectionner un look and feel, et montre à quoi ressemblent différents composants dans le look and feel choisi :

 
Sélectionnez
//: c13:LookAndFeel.java
// Sélection de divers looks & feels.
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjava.util.*;
importcom.bruceeckel.swing.*;

publicclassLookAndFeel extendsJFrame {
String[] choices = { 
   "eeny", "meeny", "minie", "moe", "toe", "you"
};
Component[] samples = {
   newJButton("JButton"),
   newJTextField("JTextField"),
   newJLabel("JLabel"),
   newJCheckBox("JCheckBox"),
   newJRadioButton("Radio"),
   newJComboBox(choices),
   newJList(choices),
};
publicLookAndFeel() {
   super("Look And Feel");
   Container cp = getContentPane();
   cp.setLayout(newFlowLayout());
   for(inti = 0; i      cp.add(samples[i]);
}
privatestaticvoidusageError() {
   System.out.println(
     "Usage:LookAndFeel [cross|system|motif]");
   System.exit(1);
}
publicstaticvoidmain(String[] args) {
   if(args.length == 0) usageError();
   if(args[0].equals("cross")) {
     try{
       UIManager.setLookAndFeel(UIManager.
         getCrossPlatformLookAndFeelClassName());
     } catch(Exception e) {
         e.printStackTrace(System.err);
     }
   } elseif(args[0].equals("system")) {
     try{
       UIManager.setLookAndFeel(UIManager.
         getSystemLookAndFeelClassName());
     } catch(Exception e) {
         e.printStackTrace(System.err);
     }
   } elseif(args[0].equals("motif")) {
     try{
       UIManager.setLookAndFeel("com.sun.java."+
         "swing.plaf.motif.MotifLookAndFeel");
     } catch(Exception e) {
         e.printStackTrace(System.err);
     }
   } elseusageError();
   // Remarquons que le look & feel doit être positionné
   // avant la création des composants.
   Console.run(newLookAndFeel(), 300, 200);
}
} ///:~

Il est également possible de créer un package de look and feel sur mesure, par exemple si on crée un environnement de travail pour une société qui désire une apparence spéciale. C'est un gros travail qui est bien au-delà de la portée de ce livre (en fait vous découvrirez qu'il est au-delà de la portée de beaucoup de livres dédiés à Swing).name="_Toc481064830">

XV-H-24. Le presse-papier [clipboard]

JFC permet des opérations limitées avec le presse-papier système (dans le package java.awt.datatransfer). On peut copier des objets String dans le presse-papier en tant que texte, et on peut coller du texte depuis le presse-papier dans des objets String. Bien sûr, le presse-papier est prévu pour contenir n'importe quel type de données, mais la représentation de ces données dans le presse-papier est du ressort du programme effectuant les opérations de couper et coller. L'API Java clipboard permet ces extensions à l'aide du concept de « parfum » [flavor]. Les données en provenance du presse-papier sont associées à un ensemble de flavors dans lesquels on peut les convertir (par exemple, un graphe peut être représenté par une chaîne de nombres ou par une image) et on peut vérifier si les données contenues dans le presse-papier acceptent le flavor qui nous intéresse.

Le programme suivant est une démonstration simple de couper, copier et coller des données String dans une face="Georgia">JTextArea. On remarquera que les séquences clavier utilisées normalement pour couper, copier et coller fonctionnent également. Mais si on observe un JTextField ou un JTextArea dans tout autre programme, on verra qu'ils acceptent aussi automatiquement les séquences clavier du presse-papier. Cet exemple ajoute simplement un contrôle du presse-papier par le programme, et on peut utiliser ces techniques pour capturer du texte du presse-papier depuis autre chose qu'un JTextComponent.

 
Sélectionnez
//: c13:CutAndPaste.java
// Utilisation du presse-papier.
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjava.awt.datatransfer.*;
importcom.bruceeckel.swing.*;

publicclassCutAndPaste extendsJFrame  {
JMenuBar mb = newJMenuBar();
JMenu edit = newJMenu("Edit");
JMenuItem
   cut = newJMenuItem("Cut"),
   copy = newJMenuItem("Copy"),
   paste = newJMenuItem("Paste");
JTextArea text = newJTextArea(20, 20);
Clipboard clipbd = 
   getToolkit().getSystemClipboard();
publicCutAndPaste()  {
   cut.addActionListener(newCutL());
   copy.addActionListener(newCopyL());
   paste.addActionListener(newPasteL());
   edit.add(cut);
   edit.add(copy);
   edit.add(paste);
   mb.add(edit);
   setJMenuBar(mb);
   getContentPane().add(text);
}
classCopyL implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     String selection = text.getSelectedText();
     if(selection == null)
       return;
     StringSelection clipString =       newStringSelection(selection);
     clipbd.setContents(clipString,clipString);
   }
}
classCutL implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     String selection = text.getSelectedText();
     if(selection == null)
       return;
     StringSelection clipString =       newStringSelection(selection);
     clipbd.setContents(clipString, clipString);
     text.replaceRange("",
       text.getSelectionStart(),
       text.getSelectionEnd());
   }
}
classPasteL implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     Transferable clipData =       clipbd.getContents(CutAndPaste.this);
     try{
       String clipString =         (String)clipData.
           getTransferData(
             DataFlavor.stringFlavor);
       text.replaceRange(clipString,
         text.getSelectionStart(),
         text.getSelectionEnd());
     } catch(Exception ex) {
       System.err.println("Not String flavor");
     }
   }
}
publicstaticvoidmain(String[] args) {
   Console.run(newCutAndPaste(), 300, 200);
}
} ///:~

La création et l'ajout du menu et du JTextArea devraient être maintenant une activité naturelle. Ce qui est différent est la création du champ Clipboard clipbd, qui est faite à l'aide du Toolkit.

Toutes les actions sont effectuées dans les listeners. Les listeners CopyL et CutL sont identiques à l'exception de la dernière ligne de CutL, qui efface la ligne qui a été copiée. Les deux lignes particulières sont la création d'un objet StringSelection à partir du String, et l'appel à setContents() avec ce StringSelection. C'est tout ce qu'il y a à faire pour mettre une String dans le presse-papier.

Dans PasteL, les données sont extraites du presse-papier à l'aide de name="Index1777">getContents(). Ce qu'il en sort est un objet Transferable assez anonyme, et on ne sait pas exactement ce qu'il contient. Un moyen de le savoir est d'appeler getTransferDataFlavors(), qui renvoie un tableau d'objets name="Index1780">DataFlavor indiquant quels flavors sont acceptés par cet objet. On peut aussi le demander directement à l'aide de isDataFlavorSupported(), en passant en paramètre le flavor qui nous intéresse. Dans ce programme, toutefois, on utilise une approche téméraire : on appelle getTransferData() en supposant que le contenu accepte le flavor String, et si ce n'est pas le cas le problème est pris en charge par le traitement d'exception.

Dans le futur, on peut s'attendre à ce qu'il y ait plus de flavors acceptés.

XV-I. Empaquetage d'une applet dans un fichier JAR

Une utilisation importante de l'utilitaire JAR est l'optimisation du chargement d'une applet. En Java 1.0, les gens avaient tendance à entasser tout leur code dans une seule classe, de sorte que le client ne devait faire qu'une seule requête au serveur pour télécharger le code de l'applet. Ceci avait pour résultat des programmes désordonnés, difficiles à lire (et à maintenir), et d'autre part le fichier .class n'était pas compressé, de sorte que le téléchargement n'était pas aussi rapide que possible.

Les fichiers JAR résolvent le problème en compressant tous les fichiers .class en un seul fichier qui est téléchargé par le navigateur. On peut maintenant avoir une conception correcte sans se préoccuper du nombre de fichiers .class qui seront nécessaires, et l'utilisateur aura un temps de téléchargement beaucoup plus court.

Prenons par exemple TicTacToe.java. Il apparaît comme une seule classe, mais en fait il contient cinq classes internes, ce qui fait six au total. Une fois le programme compilé on l'emballe dans un fichier JAR avec l'instruction :

 
Sélectionnez
jar cf TicTacToe.jar *.class

Ceci suppose que dans le répertoire courant il n'y a que les fichiers .class issus de TicTacToe.java (sinon on emporte du bagage supplémentaire).

On peut maintenant créer une page HTML avec le nouveau tag name="Index1785">archive pour indiquer le nom du fichier JAR. Voici pour exemple le tag utilisant l'ancienne forme du tag HTML :

 
Sélectionnez
<head><title>TicTacToe Example Applet
</title></head>
<body>
<applet code=TicTacToe.class
       archive=TicTacToe.jar
       width=200 height=100>
</applet>
</body>

Il faudra le mettre dans la nouvelle forme (confuse, compliquée) montrée plus haut dans ce chapitre pour le faire fonctionner.

XV-J. Techniques de programmation

La programmation de GUI en Java étant une technologie évolutive, avec des modifications très importantes entre Java 1.0/1.1 et la bibliothèque Swing, certains styles de programmation anciens ont pu s'insinuer dans des exemples qu'on peut trouver pour Swing. D'autre part, Swing permet une meilleure programmation que ce que permettaient les anciens modèles. Dans cette partie, certains de ces problèmes vont être montrés en présentant et en examinant certains styles de programmation.

XV-J-1. Lier des événements dynamiquement

Un des avantages du modèle d'événements Swing est sa flexibilité. On peut ajouter ou retirer un comportement sur événement à l'aide d'un simple appel de méthode. L'exemple suivant le montre :

 
Sélectionnez
//: c13:DynamicEvents.java
// On peut modifier dynamiquement le comportement sur événement.
// Montre également plusieurs actions pour un événement.
// <applet code=DynamicEvents
//  width=250 height=400></applet>
importjavax.swing.*;
importjava.awt.*;
importjava.awt.event.*;
importjava.util.*;
importcom.bruceeckel.swing.*;

publicclassDynamicEvents extendsJApplet {
ArrayList v = newArrayList();
inti = 0;
JButton
   b1 = newJButton("Button1",
   b2 = newJButton("Button2");
JTextArea txt = newJTextArea();
classB implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     txt.append("A button was pressed\n");
   }
}
classCountListener implementsActionListener {
   intindex;
   publicCountListener(inti) { index = i; }
   publicvoidactionPerformed(ActionEvent e) {
     txt.append("Counted Listener "+index+"\n");
   }
}
classB1 implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     txt.append("Button 1 pressed\n");
     ActionListener a = newCountListener(i++);
     v.add(a);
     b2.addActionListener(a);
   }
}
classB2 implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     txt.append("Button2 pressed\n");
     intend = v.size() - 1;
     if(end >= 0) {
       b2.removeActionListener(
         (ActionListener)v.get(end));
       v.remove(end);
     }
   }
}
publicvoidinit() {
   Container cp = getContentPane();
   b1.addActionListener(newB());
   b1.addActionListener(newB1());
   b2.addActionListener(newB());
   b2.addActionListener(newB2());
   JPanel p = newJPanel();
   p.add(b1);
   p.add(b2);
   cp.add(BorderLayout.NORTH, p);
   cp.add(newJScrollPane(txt));
}
publicstaticvoidmain(String[] args) {
   Console.run(newDynamicEvents(), 250, 400);
}
} ///:~

Les nouvelles astuces dans cet exemple sont :

  1. Il y a plus d'un listener attaché à chaque Button. En règle générale, les composants gèrent les événements en tant que multicast, ce qui signifie qu'on peut enregistrer plusieurs listeners pour un seul événement. Pour les composants spéciaux dans lesquels un événement est géré en tant que unicast, on obtiendra une exception TooManyListenersException ;
  2. Lors de l'exécution du programme, les listeners sont ajoutés et enlevés du Button b2 dynamiquement. L'ajout est réalisé de la façon vue précédemment, mais chaque composant a aussi une méthode removeXXXListener() pour enlever chaque type de listener.

Ce genre de flexibilité permet une grande puissance de programmation.

Il faut remarquer qu'il n'est pas garanti que les listeners d'événements soient appelés dans l'ordre dans lequel ils sont ajoutés (bien que la plupart des implémentations le fassent de cette façon).

XV-J-2. Séparation entre la logique applicative [business logic] et la logique de l'interface utilisateur [UI logic]

En général on conçoit les classes de manière à ce que chacune fasse une seule chose. Ceci est particulièrement important pour le code d'une interface utilisateur, car il arrive souvent qu'on lie ce qu'on fait à la manière dont on l'affiche. Ce genre de couplage empêche la réutilisation du code. Il est de loin préférable de séparer la logique applicative de la partie GUI. De cette manière, non seulement la logique applicative est plus facile à réutiliser, mais il est également plus facile de récupérer la GUI.

Un autre problème concerne les systèmes répartis [multitiered systems], dans lesquels les objets applicatifs se trouvent sur une machine séparée. Cette centralisation des règles applicatives permet des modifications ayant un effet immédiat pour toutes les nouvelles transactions, ce qui est une façon intéressante d'installer un système. Cependant, ces objets applicatifs peuvent être utilisés dans de nombreuses applications, et de ce fait ils ne devraient pas être liés à un mode d'affichage particulier. Ils devraient se contenter d'effectuer les opérations applicatives, et rien de plus.

L'exemple suivant montre comme il est facile de séparer la logique applicative du code GUI :

 
Sélectionnez
//: c13:Separation.java
// Séparation entre la logique GUI et les objets applicatifs.
// <applet code=Separation
// width=250 height=150> </applet>
importjavax.swing.*;
importjava.awt.*;
importjavax.swing.event.*;
importjava.awt.event.*;
importjava.applet.*;
importcom.bruceeckel.swing.*;

classBusinessLogic {
privateintmodifier;
publicBusinessLogic(intmod) {
   modifier = mod;
}
publicvoidsetModifier(intmod) {
   modifier = mod;
}
publicintgetModifier() {
   returnmodifier;
}
// Quelques opérations applicatives :
publicintcalculation1(intarg) {
   returnarg * modifier;
}
publicintcalculation2(intarg) {
   returnarg + modifier;
}
}

publicclassSeparation extendsJApplet {
JTextField 
   t = newJTextField(15),
   mod = newJTextField(15);
BusinessLogic bl = newBusinessLogic(2);
JButton
   calc1 = newJButton("Calculation 1",
   calc2 = newJButton("Calculation 2");
staticintgetValue(JTextField tf) {
   try{
     returnInteger.parseInt(tf.getText());
   } catch(NumberFormatException e) {
     return0;
   }
}
classCalc1L implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     t.setText(Integer.toString(
       bl.calculation1(getValue(t))));
   }
}
classCalc2L implementsActionListener {
   publicvoidactionPerformed(ActionEvent e) {
     t.setText(Integer.toString(
       bl.calculation2(getValue(t))));
   }
}
// Si vous voulez que quelque chose se passe chaque fois
// qu'un JTextField est modifié, ajoutez ce listener :
classModL implementsDocumentListener {
   publicvoidchangedUpdate(DocumentEvent e) {}
   publicvoidinsertUpdate(DocumentEvent e) {
     bl.setModifier(getValue(mod));
   }
   publicvoidremoveUpdate(DocumentEvent e) {
     bl.setModifier(getValue(mod));
   }
}
publicvoidinit() {
   Container cp = getContentPane();
   cp.setLayout(newFlowLayout());
   cp.add(t);
   calc1.addActionListener(newCalc1L());
   calc2.addActionListener(newCalc2L());
   JPanel p1 = newJPanel();
   p1.add(calc1); 
   p1.add(calc2);
   cp.add(p1);
   mod.getDocument().
     addDocumentListener(newModL());
   JPanel p2 = newJPanel();
   p2.add(newJLabel("Modifier:"));
   p2.add(mod);
   cp.add(p2);
}
publicstaticvoidmain(String[] args) {
   Console.run(newSeparation(), 250, 100);
}
} ///:~

On peut voir que BusinessLogic est une classe toute simple, qui effectue ses opérations sans même avoir idée qu'elle puisse être utilisée dans un environnement GUI. Elle se contente d'effectuer son travail.

Separation garde la trace des détails de l'interface utilisateur, et elle communique avec BusinessLogic uniquement à travers son interface public. Toutes les opérations sont concentrées sur l'échange d'informations bidirectionnelles entre l'interface utilisateur et l'objet BusinessLogic. De même, Separation fait uniquement son travail. Comme Separation sait uniquement qu'il parle à un objet BusinessLogic (c'est-à-dire qu'il n'est pas fortement couplé), il pourrait facilement être transformé pour parler à d'autres types d'objets.

Penser à séparer l'interface utilisateur de la logique applicative facilite également l'adaptation de code existant pour fonctionner avec Java.

XV-J-3. Une forme canonique

Les classes internes, le modèle d'événements de Swing, et le fait que l'ancien modèle d'événements soit toujours disponible avec de nouvelles fonctionnalités des bibliothèques qui reposent sur l'ancien style de programmation, ont ajouté un nouvel élément de confusion dans le processus de conception du code. Il y a maintenant encore plus de façons d'écrire du mauvais code.

À l'exception de circonstances qui tendent à disparaître, on peut toujours utiliser l'approche la plus simple et la plus claire : les classes listener (normalement des classes internes) pour tous les besoins de traitement d'événements. C'est la forme utilisée dans la plupart des exemples de ce chapitre.

En suivant ce modèle, on devrait pouvoir réduire les lignes de programmes qui disent : « Je me demande ce qui a provoqué cet événement ». Chaque morceau de code doit se concentrer sur une action, et non pas sur des vérifications de types. C'est la meilleure manière d'écrire le code ; c'est non seulement plus facile à conceptualiser, mais également beaucoup plus facile à lire et à maintenir.

Jusqu'ici, dans ce livre, nous avons vu que Java permet de créer des morceaux de code réutilisables. L'unité de code la plus réutilisable est la classe, car elle contient un ensemble cohérent de caractéristiques (champs) et comportements (méthodes) qui peuvent être réutilisés soit directement par combinaison, soit par héritage.

L'héritage et le polymorphisme sont des éléments essentiels de la programmation orientée objet, mais dans la majorité des cas, lorsqu'on bâtit une application, en fait on désire disposer de composants qui font exactement ce qu'on veut. On aimerait placer ces éléments dans notre conception comme l'ingénieur électronicien qui assemble des puces sur un circuit. On sent bien qu'il devrait y avoir une façon d'accélérer ce style de programmation modulaire.

La programmation visuelle est devenue très populaire d'abord avec le Visual Basic (VB) de Microsoft, ensuite avec une seconde génération d'outils, avec Delphi de Borland (l'inspiration principale de la conception des JavaBeans). Avec ces outils de programmation, les composants sont représentés visuellement, ce qui est logique, car ils affichent d'habitude un composant visuel tel qu'un bouton ou un champ de texte. La représentation visuelle est en fait souvent exactement l'aspect du composant lorsque le programme tournera. Une partie du processus de programmation visuelle consiste à faire glisser un composant d'une palette pour le déposer dans un formulaire. Pendant qu'on fait cette opération, l'outil de construction d'applications génère du code, et ce code entraînera la création du composant lors de l'exécution du programme.

Le simple fait de déposer des composants dans un formulaire ne suffit généralement pas à compléter le programme. Il faut souvent modifier les caractéristiques d'un composant, telles que sa couleur, son texte, à quelle base de données il est connecté, etc. Des caractéristiques pouvant être modifiées au moment de la conception s'appellent des propriétés [properties]. On peut manipuler les propriétés du composant dans l'outil de construction d'applications, et ces données de configuration sont sauvegardées lors de la construction du programme, de sorte qu'elles puissent être régénérées lors de son exécution.

Vous êtes probablement maintenant habitués à l'idée qu'un objet est plus que des caractéristiques ; c'est aussi un ensemble de comportements. À la conception, les comportements d'un composant visuel sont partiellement représentés par des événements [events], signifiant : « ceci peut arriver à ce composant ». En général on décide de ce qui se passera lorsqu'un événement apparaît en liant du code à cet événement.

C'est ici que se trouve le point critique du sujet : l'outil de construction d'applications utilise la réflexion pour interroger dynamiquement le composant et découvrir quelles propriétés et quels événements le composant accepte. Une fois connues, il peut afficher ces propriétés et en permettre la modification (tout en sauvegardant l'état lors de la construction du programme), et afficher également les événements. En général, on double-clique sur un événement et l'outil crée la structure du code relié à cet événement. Tout ce qu'il reste à faire est d'écrire le code qui s'exécute lorsque cet événement arrive.

Tout ceci fait qu'une bonne partie du travail est faite par l'outil de construction d'applications. On peut alors se concentrer sur l'aspect du programme et ce qu'il est supposé faire, et s'appuyer sur l'outil pour s'occuper du détail des connexions. La raison pour laquelle les outils de programmation visuels on autant de succès est qu'ils accélèrent fortement le processus de construction d'une application, l'interface utilisateur à coup sûr, mais également d'autres parties de l'application.

XV-J-4. Qu'est-ce qu'un Bean ?

Une fois la poussière retombée, un composant est uniquement un bloc de code, normalement intégré dans une classe. La clé du système est la capacité du constructeur d'applications de découvrir les propriétés et événements de ce composant. Pour créer un composant VB, le programmeur devait écrire un bout de code assez compliqué, en suivant certaines conventions pour exposer les propriétés et événements. Delphi est un outil de programmation visuelle de seconde génération, pour lequel il est beaucoup plus facile de créer un composant visuel. Java de son côté a porté la création de composants visuels à son état le plus avancé, avec les JavaBeans, car un Bean est tout simplement une classe. Il n'y a pas besoin d'écrire de code supplémentaire ou d'utiliser des extensions particulières du langage pour transformer quelque chose en Bean. La seule chose à faire, en fait, est de modifier légèrement la façon de nommer les méthodes. C'est le nom de la méthode qui dit au constructeur d'applications s'il s'agit d'une propriété, d'un événement, ou simplement une méthode ordinaire.

Dans la documentation Java, cette convention de nommage est par erreur désignée comme un modèle de conception [design pattern]. Ceci est maladroit, car les modèles de conception (voir Thinking in Patterns with Java, téléchargeable à www.BruceEckel.com) sont suffisamment difficiles à comprendre sans ajouter ce genre de confusions. Ce n'est pas un modèle de conception, c'est uniquement une convention de nommage assez simple :

  1. Pour une propriété nommée xxx, on crée deux méthodes : getXxx() et setXxx(). Remarquons que la première lettre après get ou set est transformée automatiquement en minuscule pour obtenir le nom de la propriété. Le type fourni par la méthode get est le même que le type de l'argument de la méthode set. Le nom de la propriété et le type pour les méthodes get et set ne sont pas liés ;
  2. Pour une propriété de type boolean, on peut utiliser les méthodes get et set comme ci-dessus, ou utiliser is au lieu de get ;
  3. Les méthodes ordinaires du Bean ne suivent pas la convention de nommage ci-dessus, mais elles sont public ;
  4. Pour les événements, on utilise la technique Swing du listener. C'est exactement la même chose que ce qu'on a déjà vu : addFooBarListener(FooBarListener) et removeFooBarListener(FooBarListener) pour gérer un FooBarEvent. La plupart du temps, les événements intégrés satisfont les besoins, mais on peut créer ses propres événements et interfaces listeners.

Le point 1 ci-dessus répond à la question que vous vous êtes peut-être posée en comparant un ancien et un nouveau code : un certain nombre de méthodes ont subi de petits changements de noms, apparemment sans raison. On voit maintenant que la plupart de ces changements avaient pour but de s'adapter aux conventions de nommage get et set de manière à transformer les composants en Beans.

On peut utiliser ces règles pour créer un Bean simple :

 
Sélectionnez
//: frogbean:Frog.java
// Un JavaBean trivial.
package frogbean;
import java.awt.*;
import java.awt.event.*;

class Spots {}

public class Frog {
  private int jumps;
  private Color color;
  private Spots spots;
  private boolean jmpr;
  public int getJumps() { return jumps; }
  public void setJumps(int newJumps) { 
    jumps = newJumps;
  }
  public Color getColor() { return color; }
  public void setColor(Color newColor) { 
    color = newColor; 
  }
  public Spots getSpots() { return spots; }
  public void setSpots(Spots newSpots) {
    spots = newSpots; 
  }
  public boolean isJumper() { return jmpr; }
  public void setJumper(boolean j) { jmpr = j; }
  public void addActionListener(
      ActionListener l) {
    //...
  }
  public void removeActionListener(
      ActionListener l) {
    // ...
  }
  public void addKeyListener(KeyListener l) {
    // ...
  }
  public void removeKeyListener(KeyListener l) {
    // ...
  }
  // Une méthode public "ordinaire" :
  public void croak() {
    System.out.println("Ribbet!");
  }
} ///:~

Tout d'abord, on voit qu'il s'agit d'une simple classe. En général, tous les champs seront private, et accessibles uniquement à l'aide des méthodes. En suivant la convention de nommage, les propriétés sont jumps, color, spots et jumper (remarquons le passage à la minuscule pour la première lettre du nom de la propriété). Bien que le nom de l'identificateur interne soit le même que le nom de la propriété dans les trois premiers cas, dans jumper on peut voir que le nom de la propriété n'oblige pas à utiliser un identificateur particulier pour les variables internes (ou même, en fait, d'avoir des variables internes pour cette propriété).

Les événements gérés par ce Bean sont ActionEvent et KeyEvent, basés sur le nom des méthodes add et remove pour le listener associé. Enfin on remarquera que la méthode ordinaire croak() fait toujours partie du Bean simplement parce qu'il s'agit d'une méthode public, et non parce qu'elle se conforme à une quelconque convention de nommage.

XV-J-5. Extraction des informations sur les Beans [BeanInfo] à l'aide de l'introspecteur [Introspector]

L'un des points critiques du système des Beans est le moment où on fait glisser un bean d'une palette pour le déposer dans un formulaire. L'outil de construction d'applications doit être capable de créer le Bean (il y arrive s'il existe un constructeur par défaut) et, sans accéder au code source du Bean, extraire toutes les informations nécessaires à la création de la feuille de propriétés et de traitement d'événements.

Une partie de la solution est déjà évidente depuis la fin du livre XIV : la réflexion Java permet de découvrir toutes les méthodes d'une classe anonyme. Ceci est parfait pour résoudre le problème des Beans, sans avoir accès à des mots-clefs du langage spéciaux, comme ceux utilisés dans d'autres langages de programmation visuelle. En fait, une des raisons principales d'inclure la réflexion dans Java était de permettre les Beans (bien que la réflexion serve aussi à la sérialisation des objets et à l'invocation de méthodes à distance [RMI : remote method invocation]). On pourrait donc s'attendre à ce qu'un outil de construction d'applications doive appliquer la réflexion à chaque Bean et à fureter dans ses méthodes pour trouver les propriétés et événements de ce Bean.

Ceci serait certainement possible, mais les concepteurs du langage Java voulaient fournir un outil standard, non seulement pour rendre les Beans plus faciles à utiliser, mais aussi pour fournir une plate-forme standard pour la création de Beans plus complexes. Cet outil est la classe Introspector, la méthode la plus importante de cette classe est le static getBeanInfo(). On passe la référence d'une Class à cette méthode , elle l'interroge complètement et retourne un objet BeanInfo qu'on peut disséquer pour trouver les propriétés, méthodes et événements.

Vous n'aurez probablement pas à vous préoccuper de tout ceci, vous utiliserez probablement la plupart du temps des beans prêts à l'emploi, et vous n'aurez pas besoin de connaître toute la magie qui se cache là-dessous. Vous ferez simplement glisser vos Beans dans des formulaires, vous en configurerez les propriétés et vous écrirez des traitements pour les événements qui vous intéressent. Toutefois, c'est un exercice intéressant et pédagogique d'utiliser l'Introspector pour afficher les informations sur un Bean, et voici donc un outil qui le fait :

 
Sélectionnez
//: c13:BeanDumper.java
// Introspection d'un Bean.
// <applet code=BeanDumper width=600 height=500>
// </applet>
import java.beans.*;
import java.lang.reflect.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class BeanDumper extends JApplet {
  JTextField query = 
    new JTextField(20);
  JTextArea results = new JTextArea();
  public void prt(String s) {
    results.append(s + "\n");
  }
  public void dump(Class bean){
    results.setText("");
    BeanInfo bi = null;
    try {
      bi = Introspector.getBeanInfo(
        bean, java.lang.Object.class);
    } catch(IntrospectionException e) {
      prt("Couldn't introspect " + 
        bean.getName());
      return;
    }
    PropertyDescriptor[] properties = 
      bi.getPropertyDescriptors();
    for(int i = 0; i       Class p = properties[i].getPropertyType();
      prt("Property type:\n  " + p.getName() +
        "Property name:\n  " + 
        properties[i].getName());
      Method readMethod = 
        properties[i].getReadMethod();
      if(readMethod != null)
        prt("Read method:\n  " + readMethod);
      Method writeMethod = 
        properties[i].getWriteMethod();
      if(writeMethod != null)
        prt("Write method:\n  " + writeMethod);
      prt("====================");
    }
    prt("Public methods:");
    MethodDescriptor[] methods =      bi.getMethodDescriptors();
    for(int i = 0; i       prt(methods[i].getMethod().toString());
    prt("======================");
    prt("Event support:");
    EventSetDescriptor[] events = 
      bi.getEventSetDescriptors();
    for(int i = 0; i       prt("Listener type:\n  " +
        events[i].getListenerType().getName());
      Method[] lm = 
        events[i].getListenerMethods();
      for(int j = 0; j         prt("Listener method:\n  " +
          lm[j].getName());
      MethodDescriptor[] lmd = 
        events[i].getListenerMethodDescriptors();
      for(int j = 0; j         prt("Method descriptor:\n  " +
          lmd[j].getMethod());
      Method addListener = 
        events[i].getAddListenerMethod();
      prt("Add Listener Method:\n  " +
          addListener);
      Method removeListener =        events[i].getRemoveListenerMethod();
      prt("Remove Listener Method:\n  " +
        removeListener);
      prt("====================");
    }
  }
  class Dumper implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String name = query.getText();
      Class c = null;
      try {
        c = Class.forName(name);
      } catch(ClassNotFoundException ex) {
        results.setText("Couldn't find " + name);
        return;
      }
      dump(c);
    }
  }      
  public void init() {
    Container cp = getContentPane();
    JPanel p = new JPanel();
    p.setLayout(new FlowLayout());
    p.add(new JLabel("Qualified bean name:"));
    p.add(query);
    cp.add(BorderLayout.NORTH, p);
    cp.add(new JScrollPane(results));
    Dumper dmpr = new Dumper();
    query.addActionListener(dmpr);
    query.setText("frogbean.Frog");
    // Force evaluation
    dmpr.actionPerformed(
      new ActionEvent(dmpr, 0, ""));
  }
  public static void main(String[] args) {
    Console.run(new BeanDumper(), 600, 500);
  }
} ///:~

BeanDumper.dump() est la méthode qui fait tout le travail. Il essaie d'abord de créer un objet BeanInfo, et en cas de succès il appelle les méthodes de BeanInfo qui fournissent les informations sur les propriétés, méthodes et événements. Dans Introspector.getBeanInfo(), on voit qu'il y a un second argument. Celui-ci dit à l'Introspector où s'arrêter dans la hiérarchie d'héritage. Ici, il s'arrête avant d'analyser toutes les méthodes d'Object, parce qu'elles ne nous intéressent pas.

Pour les propriétés, getPropertyDescriptors() renvoie un tableau de PropertyDescriptors. Pour chaque PropertyDescriptor, on peut appeler getPropertyType() pour connaître la classe d'un objet passé par les méthodes de propriétés. Ensuite, on peut obtenir le nom de chaque propriété (issu du nom des méthodes) à l'aide de getName(), la méthode pour la lire à l'aide de getReadMethod(), et la méthode pour la modifier à l'aide de getWriteMethod(). Ces deux dernières méthodes retournent un objet Method qui peut être utilisé pour appeler la méthode correspondante de l'objet (ceci fait partie de la réflexion).

Pour les méthodes public (y compris les méthodes des propriétés), getMethodDescriptors() renvoie un tableau de MethodDescriptors. Pour chacun de ces descripteurs, on peut obtenir l'objet Method associé, et imprimer son nom.

Pour les événements, getEventSetDescriptors() renvoie un tableau de (que pourrait-il renvoyer d'autre ?) EventSetDescriptors. Chacun de ces descripteurs peut être utilisé pour obtenir la classe du listener, les méthodes de cette classe listener, et les méthodes pour ajouter et enlever ce listener. Le programme BeanDumper imprime toutes ces informations.

Au démarrage, le programme force l'évaluation de frogbean.Frog. La sortie, après suppression de détails inutiles ici, est :

 
Sélectionnez
class name: Frog
Property type:
  Color
Property name:
  color
Read method:
  public Color getColor()
Write method:
  public void setColor(Color)
====================Property type:
  Spots
Property name:
  spots
Read method:
  public Spots getSpots()
Write method:
  public void setSpots(Spots)
====================Property type:
  boolean
Property name:
  jumper
Read method:
  public boolean isJumper()
Write method:
  public void setJumper(boolean)
====================Property type:
  int
Property name:
  jumps
Read method:
  public int getJumps()
Write method:
  public void setJumps(int)
====================Public methods:
public void setJumps(int)
public void croak()
public void removeActionListener(ActionListener)
public void addActionListener(ActionListener)
public int getJumps()
public void setColor(Color)
public void setSpots(Spots)
public void setJumper(boolean)
public boolean isJumper()
public void addKeyListener(KeyListener)
public Color getColor()
public void removeKeyListener(KeyListener)
public Spots getSpots()
======================Event support:
Listener type:
  KeyListener
Listener method:
  keyTyped
Listener method:
  keyPressed
Listener method:
  keyReleased
Method descriptor:
  public void keyTyped(KeyEvent)
Method descriptor:
  public void keyPressed(KeyEvent)
Method descriptor:
  public void keyReleased(KeyEvent)
Add Listener Method:
  public void addKeyListener(KeyListener)
Remove Listener Method:
  public void removeKeyListener(KeyListener)
====================Listener type:
  ActionListener
Listener method:
  actionPerformed
Method descriptor:
  public void actionPerformed(ActionEvent)
Add Listener Method:
  public void addActionListener(ActionListener)
Remove Listener Method:
  public void removeActionListener(ActionListener)
====================

La liste des méthodes public contient les méthodes qui ne sont pas associées à une propriété ou un événement, telles que croak(), ainsi que celles qui le sont. Ce sont toutes les méthodes pouvant être appelées par programme pour un Bean, et l'outil de construction d'applications peut décider de les lister toutes lors de la création des appels de méthodes, pour nous faciliter la tâche.

Enfin, on peut voir que les événements sont tous triés entre le listener, ses méthodes et les méthodes pour ajouter et supprimer les listeners. Fondamentalement, une fois obtenu le BeanInfo, on peut trouver tout ce qui est important pour un Bean. On peut également appeler les méthodes de ce Bean, bien qu'on n'ait aucune autre information à l'exception de l'objet (ceci est également une caractéristique de la réflexion).

XV-J-6. Un Bean plus complexe

L'exemple suivant est un peu plus compliqué, bien que futile. Il s'agit d'un JPanel qui dessine un petit cercle autour de la souris chaque fois qu'elle se déplace. Lorsqu'on clique, le mot Bang! apparaît au milieu de l'écran, et un action listener est appelé.

Les propriétés modifiables sont la taille du cercle, ainsi que la couleur, la taille et le texte du mot affiché lors du clic. Un BangBean a également ses propres addActionListener() et removeActionListener(), de sorte qu'on peut y attacher son propre listener qui sera appelé lorsque l'utilisateur clique sur le BangBean. Vous devriez être capables de reconnaître le support des propriétés et événements :

 
Sélectionnez
//: bangbean:BangBean.java
// Un Bean graphique.
package bangbean;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import com.bruceeckel.swing.*;

public class BangBean extends JPanel
     implements Serializable {
  protected int xm, ym;
  protected int cSize = 20; // Taille du cercle
  protected String text = "Bang!";
  protected int fontSize = 48;
  protected Color tColor = Color.red;
  protected ActionListener actionListener;
  public BangBean() {
    addMouseListener(new ML());
    addMouseMotionListener(new MML());
  }
  public int getCircleSize() { return cSize; }
  public void setCircleSize(int newSize) {
    cSize = newSize;
  }
  public String getBangText() { return text; }
  public void setBangText(String newText) {
    text = newText;
  }
  public int getFontSize() { return fontSize; }
  public void setFontSize(int newSize) {
    fontSize = newSize;
  }
  public Color getTextColor() { return tColor; }
  public void setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.black);
    g.drawOval(xm - cSize/2, ym - cSize/2, 
      cSize, cSize);
  }
  // Ceci est un "unicast listener", qui est
  // la forme la plus simple de gestion des listeners :
  public void addActionListener (
      ActionListener l) 
        throws TooManyListenersException {
    if(actionListener != null)
      throw new TooManyListenersException();
    actionListener = l;
  }
  public void removeActionListener(
      ActionListener l) {
    actionListener = null;
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font(
          "TimesRoman", Font.BOLD, fontSize));
      int width = 
        g.getFontMetrics().stringWidth(text);
      g.drawString(text, 
        (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      // Appel de la méthode du listener :
      if(actionListener != null)
        actionListener.actionPerformed(
          new ActionEvent(BangBean.this,
            ActionEvent.ACTION_PERFORMED, null));
    }
  }
  class MML extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public Dimension getPreferredSize() {
    return new Dimension(200, 200);
  }
} ///:~

La première chose qu'on remarquera est que BangBean implémente l'interface Serializable. Ceci signifie que l'outil de construction d'applications peut conserver toutes les informations sur le BangBean en utilisant la sérialisation, lorsque les valeurs des propriétés ont été ajustées par l'utilisateur. Lors de la création du Bean au moment de l'exécution du programme, ces propriétés sauvegardées sont restaurées de manière à obtenir exactement ce qu'elles valaient lors de la conception.

On peut voir que tous les champs sont private, ce qu'on fait en général avec un Bean : autoriser l'accès uniquement à travers des méthodes, normalement en utilisant le système de propriétés.

En observant la signature de addActionListener(), on voit qu'il peut émettre une TooManyListenersException. Ceci indique qu'il est unicast, ce qui signifie qu'il signale à un seul listener l'arrivée d'un événement. En général on utilise des événements multicast, de sorte que de nombreux listeners puissent être notifiés de l'arrivée d'un événement. Cependant on entre ici dans des problèmes que vous ne pouvez pas comprendre avant le chapitre suivant ; on en reparlera donc (sous le titre «JavaBeans revisited»). Un événement unicast contourne le problème.

Lorsqu'on clique avec la souris, le texte est placé au milieu du BangBean, et si le champ de l'actionListener n'est pas nul, on appelle son actionPerformed(), ce qui crée un nouvel objet ActionEvent. Lorsque la souris est déplacée, ses nouvelles coordonnées sont lues et le panneau est redessiné (ce qui efface tout texte sur ce panneau, comme on le remarquera).

Voici la classe BangBeanTest permettant de tester le bean en tant qu'applet ou en tant qu'application :

 
Sélectionnez
//: c13:BangBeanTest.java
// <applet code=BangBeanTest 
// width=400 height=500></applet>
import bangbean.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;

public class BangBeanTest extends JApplet {
  JTextField txt = new JTextField(20);
  // Affiche les actions lors des tests :
  class BBL implements ActionListener {
    int count = 0;
    public void actionPerformed(ActionEvent e){
      txt.setText("BangBean action "+ count++);
    }
  }
  public void init() {
    BangBean bb = new BangBean();
    try {
      bb.addActionListener(new BBL());
    } catch(TooManyListenersException e) {
      txt.setText("Too many listeners");
    }
    Container cp = getContentPane();
    cp.add(bb);
    cp.add(BorderLayout.SOUTH, txt);
  }
  public static void main(String[] args) {
    Console.run(new BangBeanTest(), 400, 500);
  }
} ///:~

Lorsqu'un Bean est dans un environnement de développement, cette classe n'est pas utilisée, mais elle est utile pour fournir une méthode de test rapide pour chacun de nos Beans. BangBeanTest place un BangBean dans une applet, attache un simple ActionListener au BangBean pour afficher un compteur d'événements dans le JTextField chaque fois qu'un ActionEvent arrive. En temps normal, bien sûr, l'outil de construction d'applications créerait la plupart du code d'utilisation du Bean.

Lorsqu'on utilise le BangBean avec le BeanDumper, ou si on place le BangBean dans un environnement de développement acceptant les Beans, on remarquera qu'il y a beaucoup plus de propriétés et d'actions que ce qui n'apparaît dans le code ci-dessus. Ceci est dû au fait que BangBean hérite de JPanel, et comme JPanel est également un Bean, on en voit également ses propriétés et événements.

XV-J-7. Empaquetage d'un Bean

Pour installer un Bean dans un outil de développement visuel acceptant les Beans, le Bean doit être mis dans le conteneur standard des Beans, qui est un fichier JAR contenant toutes les classes, ainsi qu'un fichier manifest qui dit « ceci est un Bean ». Un fichier manifest est un simple fichier texte avec un format particulier. Pour le BangBean, le fichier manifest ressemble à ceci (sans la première et la dernière ligne) :

 
Sélectionnez
//:! :BangBean.mf
Manifest-Version: 1.0

Name: bangbean/BangBean.class
Java-Bean: True
///:~

La première ligne indique la version de la structure du fichier manifest, qui est 1.0 jusqu'à nouvel ordre de chez Sun. La deuxième ligne (les lignes vides étant ignorées) nomme le fichier BangBean.class, et la troisième précise que c'est un Bean. Sans la troisième ligne, l'outil de construction de programmes ne reconnaîtra pas la classe en tant que Bean.

Le seul point délicat est de s'assurer d'avoir le bon chemin dans le champ « Name: ». Si on retourne à BangBean.java, on voit qu'il est dans le package bangbean (et donc dans un sous-répertoire appelé bangbean qui est en dehors du classpath), et le nom dans le fichier manifest doit contenir cette information de package. De plus, il faut placer le fichier manifest dans le répertoire au-dessus de la racine du package, ce qui dans notre cas veut dire le placer dans le répertoire au-dessus du répertoire bangbean. Ensuite il faut lancer jar depuis le répertoire où se trouve le fichier manifest, de la façon suivante :

 
Sélectionnez
jar cfm BangBean.jar BangBean.mf bangbean

Ceci suppose qu'on veut que le fichier JAR résultant s'appelle BangBean.jar, et qu'on a placé le fichier manifest dans un fichier appelé BangBean.mf.

On peut se demander ce qui se passe pour les autres classes générées lorsqu'on a compilé BangBean.java. Eh bien, elles ont toutes abouti dans le sous-répertoire bangbean. Lorsqu'on donne à la commande jar le nom d'un sous-répertoire, il embarque tout le contenu de ce sous-répertoire dans le fichier jar (y compris, dans notre cas, le code source original BangBean.java qu'on peut ne pas vouloir inclure dans les Beans). Si on regarde à l'intérieur du fichier JAR, on découvre que le fichier manifest n'y est pas, mais que jar a créée son propre fichier manifest (partiellement sur la base du nôtre) appelé MANIFEST.MF et l'a placé dans le sous-répertoire META-INF (pour meta-information). Si on ouvre ce fichier manifest on remarquera également qu'une information de signature numérique a été ajoutée par jar pour chaque fichier, de la forme :

 
Sélectionnez
Digest-Algorithms: SHA MD5 
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==

En général, on ne se préoccupe pas de tout ceci, et lors de modifications il suffit de modifier le fichier manifest d'origine et rappeler jar pour créer un nouveau fichier JAR pour le Bean. On peut aussi ajouter d'autres Beans dans le fichier JAR en ajoutant simplement leurs informations dans le manifest.

Une chose à remarquer est qu'on mettra probablement chaque Bean dans son propre sous-répertoire, puisque lorsqu'on crée un fichier JAR on passe à la commande jar le nom d'un sous-répertoire et qu'il place tout ce qui est dans ce répertoire dans le fichier JAR. On peut voir que Frog et BangBean sont chacun dans leur propre sous-répertoire.

Une fois le Bean convenablement inséré dans un fichier JAR, on peut l'installer dans un environnement de développement de programmes acceptant les Beans. La façon de le faire varie d'un outil à l'autre, mais Sun fournit gratuitement un banc de test pour les JavaBeans dans leur Beans Development Kit (BDK) appelé la beanbox (le BDK se télécharge à partir de java.sun.com/beans). Pour placer un Bean dans la beanbox, il suffit de copier le fichier JAR dans le sous-répertoire jars du BDK avant de lancer la beanbox.

XV-J-8. Un support des Beans plus sophistiqué

On a vu comme il était simple de fabriquer un Bean. Mais on n'est pas limité à ce qu'on a vu ici. L'architecture des JavaBeans fournit un point d'entrée simple, mais peut aussi s'adapter à des cas plus complexes. Ceux-ci ne sont pas du ressort de ce livre, mais on va les introduire rapidement ici. On trouvera plus de détails à java.sun.com/beans.

Un endroit où l'on peut apporter des perfectionnements est le traitement des propriétés. Les exemples ci-dessus n'ont montré que des propriétés uniques, mais il est également possible de représenter plusieurs propriétés dans un tableau. C'est ce qu'on appelle une propriété indexée [indexed property]. Il suffit de fournir les méthodes appropriées (également en suivant une convention de nommage pour les noms de méthodes) et l'Introspector reconnaît une propriété indexée, de sorte qu'un outil de construction d'applications puisse y répondre correctement.

Les propriétés peuvent être liées [bound], ce qui signifie qu'elles avertiront les autres objets à l'aide d'un PropertyChangeEvent. Les autres objets peuvent alors décider de se modifier eux-mêmes suite à la modification de ce Bean.

Les propriétés peuvent être contraintes [constrained], ce qui signifie que les autres objets peuvent mettre leur veto sur la modification de cette propriété si c'est inacceptable. Les autres objets sont avertis à l'aide d'un PropertyChangeEvent, et ils peuvent émettre un PropertyVetoException pour empêcher la modification et pour rétablir les anciennes valeurs.

On peut également modifier la façon de représenter le Bean lors de la conception :

  1. On peut fournir une feuille de propriétés spécifique pour un Bean particulier. La feuille de propriétés normale sera utilisée pour tous les autres Beans, mais la feuille spéciale sera automatiquement appelée lorsque ce Bean sera sélectionné ;
  2. On peut créer un éditeur spécifique pour une propriété particulière, de sorte que la feuille de propriétés normale est utilisée, mais si on veut éditer cette propriété, c'est cet éditeur qui est automatiquement appelé ;
  3. On peut fournir une classe BeanInfo spécifique pour un Bean donné, pour fournir des informations différentes de celles créées par défaut par l'Introspector ;
  4. Il est également possible de valider ou dévalider le mode expert dans tous les FeatureDescriptors pour séparer les caractéristiques de base de celles plus compliquées.

Davantage sur les Beans

Il y a un autre problème qui n'a pas été traité ici. Chaque fois qu'on crée un Bean, on doit s'attendre à ce qu'il puisse être exécuté dans un environnement multithread. Ceci signifie qu'il faut comprendre les problèmes du threading, qui sera présenté au titre XVI. On y trouvera un paragraphe appelé «JavaBeans revisited» qui parlera de ce problème et de sa solution.

Il y a plusieurs livres sur les JavaBeans, par exemple JavaBeans par Elliotte Rusty Harold (IDG, 1998).

XV-K. Résumé

De toutes les bibliothèques Java, c'est la bibliothèque de GUI qui a subi les changements les plus importants de Java 1.0 à Java 2. L'AWT de Java 1.0 était nettement critiqué comme étant une des moins bonnes conceptions jamais vues, et bien qu'il permette de créer des programmes portables, la GUI résultante était aussi médiocre sur toutes les plateformes. Il était également limité, malaisé et peu agréable à utiliser en comparaison des outils de développement natifs disponibles sur une plate-forme donnée.

Lorsque Java 1.1 introduisit le nouveau modèle d'événements et les JavaBeans, la scène était installée. Il était désormais possible de créer des composants de GUI pouvant être facilement glissés et déposés à l'intérieur d'outils de développement visuels. De plus, la conception du modèle d'événements et des Beans montre l'accent mis sur la facilité de programmation et la maintenabilité du code (ce qui n'était pas évident avec l'AWT du 1.0). Mais le travail ne s'est terminé qu'avec l'apparition des classes JFC/Swing. Avec les composants Swing, la programmation de GUI toutes plateformes devient une expérience civilisée.

En fait, la seule chose qui manque est l'outil de développement, et c'est là que se trouve la révolution. Visual Basic et Visual C++ de Microsoft nécessitent des outils de développement de Microsoft, et il en est de même pour Delphi et C++ Builder de Borland. Si on désire une amélioration de l'outil, on n'a plus qu'à croiser les doigts et espérer que le fournisseur le fera. Mais Java est un environnement ouvert, et de ce fait, non seulement il permet la compétition des outils, mais il l'encourage. Et pour que ces outils soient pris au sérieux, ils doivent permettre l'utilisation des JavaBeans. Ceci signifie un terrain de jeu nivelé : si un meilleur outil de développement apparaît, on n'est pas lié à celui qu'on utilisait jusqu'alors, on peut migrer vers le nouvel outil et augmenter sa productivité. Cet environnement compétitif pour les outils de développement ne s'était jamais vu auparavant, et le marché résultant ne peut que générer des résultats positifs pour la productivité du programmeur.

Ce chapitre était uniquement destiné à vous fournir une introduction à la puissance de Swing et vous faire démarrer, et vous avez pu voir comme il était simple de trouver son chemin à travers les bibliothèques. Ce qu'on a vu jusqu'ici suffira probablement pour une bonne part à vos besoins en développement d'interfaces utilisateurs. Cependant, Swing ne se limite pas qu'à cela. Il est destiné à être une boîte à outils de conception d'interfaces utilisateur très puissante. Il y a probablement une façon de faire à peu près tout ce qu'on peut imaginer.

Si vous ne trouvez pas ici ce dont vous avez besoin, fouillez dans la documentation en ligne de Sun, et recherchez sur le Web, et si cela ne suffit pas, cherchez un livre consacré à Swing. Un bon endroit pour démarrer est The JFC Swing Tutorial, par Walrath & Campione (Addison Wesley, 1999).

XV-L. Exercices

Les solutions des exercices sélectionnés se trouvent dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour une faible somme depuis www.BruceEckel.com.

  1. Créer une applet/application utilisant la classe Console comme montré dans ce chapitre. Inclure un champ texte et trois boutons. Lorsqu'on appuie sur chaque bouton, faire afficher un texte différent dans le champ texte.
  2. Ajouter une boîte à cocher à l'applet de l'exercice 1, capturer l'événement, et insérer un texte différent dans le champ texte.
  3. Créer une applet/application utilisant Console. Dans la documentation HTML de java.sun.com, trouver le JPasswordField et l'ajouter à ce programme. Si l'utilisateur tape le mot de passe correct, utiliser JOptionPane pour fournir un message de succès à l'utilisateur.
  4. Créer une applet/application utilisant Console, et ajouter tous les composants qui ont une méthode addActionListener() (rechercher celles-ci dans la documentation HTML de java.sun.com; conseil : utiliser l'index). Capturer ces événements et afficher un message approprié pour chacun dans un champ texte.
  5. Créer une applet/application utilisant Console, avec un JButton et un JTextField. Écrire et attacher le listener approprié de sorte que si le bouton a le focus, les caractères tapés dessus apparaissent dans le JTextField.
  6. Créer une applet/application utilisant Console. Ajouter à la fenêtre principale tous les composants décrits dans ce chapitre, y compris les menus et une boîte de dialogue.
  7. Modifier TextFields.java de sorte que les caractères de t2 gardent la casse qu'ils avaient lorsqu'ils ont été tapés, plutôt que les forcer automatiquement en majuscules.
  8. Rechercher et télécharger un ou plusieurs des environnements de développement de GUI disponibles sur Internet, ou acheter un produit du commerce. Découvrir ce qu'il faut faire pour ajouter Bangbean à cet environnement et pour l'utiliser.
  9. Ajouter Frog.class au fichier manifest comme montré dans ce chapitre, et lancer jar pour créer un fichier JAR contenant à la fois Frog et BangBean. Ensuite, télécharger et installer le BDK de Sun ou utiliser un outil de développement admettant les Beans et ajouter le fichier JAR à votre environnement de manière à pouvoir tester les deux Beans.
  10. Créer un JavaBean appelé Valve qui contient deux propriétés : un boolean appelé on et un int appelé level. Créer un fichier manifest, utiliser jar pour empaqueter le Bean, puis le charger dans la beanbox ou dans un outil de développement acceptant les Beans, de manière à pouvoir le tester.
  11. Modifier MessageBoxes.java de manière à ce qu'il ait un ActionListener individuel pour chaque bouton (au lieu d'un correspondant au texte du bouton).
  12. Surveiller un nouveau type d'événement dans TrackEvent.java en ajoutant le nouveau code de traitement de l'événement. Il faudra découvrir vous-même le type d'événement que vous voulez surveiller.
  13. Créer un nouveau type de bouton hérité de JButton. Chaque fois qu'on appuie sur le bouton, celui-ci doit modifier sa couleur selon une couleur choisie de façon aléatoire. Voir ColorBoxes.java au titre XVI pour un exemple de la manière de générer aléatoirement une valeur de couleur.
  14. Modifier TextPane.java de manière à utiliser un JTextArea à la place du JTextPane.
  15. Modifier Menus.java pour utiliser des boutons radio au lieu de boîtes à cocher dans les menus.
  16. Simplifier List.java en passant le tableau au constructeur et en éliminant l'ajout dynamique d'éléments à la liste.
  17. Modifier SineWave.java pour transformer SineDraw en JavaBean en lui ajoutant des méthodes get et set.
  18. Vous vous souvenez du jouet permettant de faire des dessins avec deux boutons, un qui contrôle le mouvement vertical, et un qui contrôle le mouvement horizontal ? En créer un, en utilisant SineWave.java comme point de départ. À la place des boutons, utiliser des curseurs. Ajouter un bouton d'effacement de l'ensemble du dessin.
  19. Créer un indicateur de progression asymptotique qui ralentit au fur et à mesure qu'il s'approche de la fin. Ajouter un comportement aléatoire de manière à donner l'impression qu'il se remet à accélérer.
  20. Modifier Progress.java de manière à utiliser un listener plutôt que le partage du modèle pour connecter le curseur et la barre de progression.
  21. Suivre les instructions du paragraphe « Empaquetage d'une applet dans un fichier JAR » pour placer TicTacToe.java dans un fichier JAR. Créer une page HTML avec la version brouillonne et compliquée du tag applet, et la modifier pour utiliser le tag archive de manière à utiliser le fichier JAR (conseil : commencer par utiliser la page HTML pour TicTacToe.java qui est fournie avec le code source pour ce livre).
  22. Créer une applet/application utilisant Console. Celle-ci doit avoir trois curseurs, un pour le rouge, un pour le vert et un pour le bleu de java.awt.Color. Le reste du formulaire sera un JPanel qui affiche la couleur fixée par les trois curseurs. Ajouter également des champs textes non modifiables qui indiquent les valeurs courantes des valeurs RGB.
  23. Dans la documentation HTML de javax.swing, rechercher le JColorChooser. Écrire un programme avec un bouton qui ouvre le sélectionneur de couleur comme un dialogue.
  24. Presque tous les composants Swing sont dérivés de Component, qui a une méthode setCursor(). Rechercher ceci dans la documentation HTML Java . Créer une applet et modifier le curseur selon un des curseurs disponibles dans la classe Cursor.
  25. En prenant comme base ShowAddListeners.java, créer un programme avec toutes les fonctionnalités de ShowMethodsClean.java du livre XIV.

précédentsommairesuivant
Ceci est un exemple du modèle de conception [design pattern] appelé la méthode du modèle [template method].
On suppose que le lecteur connaît les bases du HTML. Il n'est pas très difficile à comprendre, et il y a beaucoup de livres et de ressources disponibles.
Cette page (en particulier la partie clsid) semblait bien fonctionner avec le JDK1.2.2 et le JDK1.3rc-1. Cependant il se peut que vous ayez parfois besoin de changer le tag dans le futur. Des détails peuvent être trouvés à java.sun.com.
Selon moi. Et après avoir étudié Swing, vous n'aurez plus envie de perdre votre temps sur les parties plus anciennes.
Comme décrit plus avant, Frame était déjà utilisé par AWT, de sorte que Swing utilise JFrame.
Ceci s'éclaircira après avoir avancé dans ce chapitre. D'abord faire de la référence JApplet un membre static de la classe (au lieu d'une variable locale de main()), et ensuite appeler applet.stop() et applet.destroy() dans WindowAdapter.windowClosing() avant d'appeler System.exit().
Il n'y a pas de MouseMotionEvent bien qu'il semble qu'il devrait y en avoir un. Le cliquage et le déplacement sont combinés dans MouseEvent, de sorte que cette deuxième apparition de MouseEvent dans le tableau n'est pas une erreur.
En Java 1.0/1.1 on ne pouvait pas hériter de l'objet bouton de façon exploitable. C'était un des nombreux défauts de conception.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.