Interface-utilisateur graphique
d’une application Java
DUT Informatique 1ère année
Cyril Pain-Barre, Sébastien Nedjar, Henri Garreta
TP 3b - Une boîte de saisie
13/05/2014 - 10:00
 
Les exercices précédents ont montré comment utiliser les trois gestionnaires de disposition fondamentaux, FlowLayout, BorderLayout et GridLayout, mais n’ont sans doute pas donné le sentiment qu’avec ces outils on peut construire des interfaces comme on en voit couramment dans les applications.

Pour découvrir comment cela peut se faire, vous allez réaliser ici une boîte de dialogue servant à acquérir et modifier des informations sur les membres d’un club où on pratique divers sports. D’abord on s’occupera de l’aspect graphique de l’interface, qui doit ressembler à ceci :

Boîte de saisie

Dans un deuxième temps on ajoutera le code nécessaire pour récupérer et valider les informations saisies, qu’on mémorisera dans une table associative (Map), constamment affichée. De plus, on se donnera la possibilité de sauvegarder cette structure de données dans un fichier et de la restaurer si nécessaire.

Enfin, pour terminer l’exercice, on refera le travail de conception de l’interface en utilisant WindowBuilder, un outil incorporé à eclipse, qui permet de composer les interfaces graphiques en plaçant avec la souris des composants présentés dans des palettes.

3b.1  Représenter les sports

Une petite question préalable que nous pouvons régler avant de nous attaquer à l’interface graphique est celui de la représentation des différents sports qui peuvent être pratiqués dans notre club.

On pourrait faire cela avec des nombres (0 pour Tennis, 1 pour Squash, 2 pour Natation, etc.) mais cela produirait des affichages très peu expressifs, genre « sportsChoisis = [ 1, 2, 4, 5, 6 ] ». On pourrait préférer des chaînes de caractères ("Tennis", "Squash", "Natation", etc.), les affichages seraient plus parlants mais les chaînes sont lourdes (des tableaux !) et dangereuses ("Tennis" n’est pas la même chose que "tennis", etc.). Or, depuis Java 5, nous disposons d’une sorte de types exactement adaptés à ce besoin, les types énumérations ou enum.

Créez un fichier séparé, nommé Sports.java, contenant la déclaration

/*
 * Les sports possibles
 */
public enum Sports {
    Tennis, Squash, Natation, Athletisme, Randonnee, Foot, Basket, Volley, Petanque;
 
    public static final int NOMBRE = values().length;
}

Les enum ont des propriétés intéressantes (voir « Le langage Java », section 12.1, page 149). Par exemple, la méthode values() renvoie tous les éléments du type dans un tableau ; ainsi, la valeur de Sports.values()[2] est Sports.Natation.

En outre, ces types supportent les ensembles (type Set<Sports>) et possèdent une méthode toString() opérationnelle.

3b.2  Mettre en place les composants graphiques

À titre d’entraînement nous commencerons par construire l’interface montrée plus haut sous forme de panneau (JPanel) mis comme panneau de contenu (content pane) d’un cadre (JFrame). Les composants seront sans effet, il ne s’agit ici que de mettre au point l’apparence graphique de l’interface.

Définissez une classe CadreSaisieMembre, sous-classe de JFrame, comportant la méthode main et le constructeur, certes peu original, suivants :

public class CadreSaisieMembre extends JFrame {

    public static void main(String[] args) {
        JFrame cadre = new CadreSaisieMembre("Test du cadre de saisie");
        cadre.setLocationRelativeTo(null);
        cadre.setVisible(true);
    }

    public CadreSaisieMembre(String titre) {
        super(titre);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setContentPane(panneauDeContenu());
        pack();
    }

    JPanel panneauDeContenu() {
        ...
    }
} 

Bien sûr, le gros du travail travail sera de définir la méthode panneauDeContenu(). Elle construit et renvoie un panneau portant toute l’interface graphique (sauf le bord, qui appartient au cadre).

Il s’agit de gérer la taille et l’emplacement des composants. La question importante qu’on doit se poser pour commencer est « lorsque l’utilisateur modifiera la taille du cadre, à qui doit profiter l’augmentation de la largeur ? et l’augmentation de la hauteur ? » Les choix suivants paraissent raisonnables :

Tout cela amène un premier niveau d’organisation du panneau en construction : un gestionnaire BorderLayout gérant trois panneaux, appelés par exemple grandCentre, grandSud et grandEst, comme sur la figure suivante :

Premier niveau

Le panneau grandCentre porte neuf composants :

Ce qui nous amène à organiser le panneau grandCentre ainsi : un BorderLayout gérant trois composants, deux panneaux (petitNord et petitSud) et une zone de texte (zoneAdresse semble un nom plus intéressant que petitCentre), comme sur la figure suivante :

Interface complexe, niveau 2

Enfin, deux GridLayout à une colonne et deux FlowLayout sont nécessaires pour gérer les composants restants (cases à cocher, boutons, etc.) :

Interface complexe, niveau 3

Maintenant que tous les panneaux et leurs gestionnaires de disposition sont en place il ne vous reste plus qu’à créer les autres composants (des JLabel, JTextField, JTextArea, JButton, JRadioButton, JCheckBox, etc.) et à les placer dans les panneaux correspondants.

Notes

  1. Pendant le développement de l’interface il peut être intéressant de donner des couleurs distinctes aux panneaux utilisés, afin de s’assurer qu’ils se disposent correctement.
  2. Pour la synchronisation des deux boutons-radio (« Homme » et « Femme ») vous devrez créer un composant (invisible) de type ButtonGroup et lui raccorder (méthode add) les deux boutons-radio.
  3. Pour que les deux boutons (« OK » et « Annuler ») aient la même taille, alors que par défaut le second est certainement plus large que le premier, vous pouvez prendre (get) la taille préférée du second et la donner (set) au premier.
  4. Il est simple et pratique de représenter les boîtes à cocher associées aux sports par un tableau de JCheckBox. N’oubliez pas alors qu’il faudra deux niveaux d’allocation : d’une part vous devrez créer le tableau :
    JCheckBox[] casesACocherSports = new JCheckBox[Sports.NOMBRE]; 
    d’autre part, puisque ce sont des objets, vous devrez créer un par un les éléments du tableau :
    for (int i = 0; i < Sports.NOMBRE; i++)
        casesACocherSports[i] = new JCheckBox(Sports.values()[i].toString());

Marges et titres

Il est parfois nécessaire de montrer graphiquement qu’un ensemble de composants sont regroupés ensemble et, éventuellement, chapeautés par un titre commun. On obtient cela en donnant une bordure (méthode setBorder(Border b)) au panneau qui contient les composants en question.

La manière la plus simple de construire des bordures consiste à appeler des méthodes statiques de la classe « fabrique » BorderFactory. Les bordures les plus fréquemment utilisées sont biseautées (beveled), gravées (etched) ou vides (empty, cela met de la marge autour de quelque chose) ; de plus, on peut titrer (titled) n’importe quel autre type de bordure. Enfin, la création d’une bordure composée (compound) permet d’associer deux bordures pour qu’elle se présentent comme une seule.

Ajoutez à la classe précédemment réalisée ce qu’il faut afin que l’interface ressemble à ceci :

3b.3  Récupérer les informations saisies

Introduction

Il s’agit maintenant d’utiliser l’interface réalisée pour acquérir effectivement des informations sur les membres du club.

On mémorisera ces dernières dans une table associative (type Map) composée de paires (clé, valeur) où la clé est une chaîne de caractères formée avec le nom et le prénom du membre et la valeur une instance d’une nouvelle classe Membre, définie à cet effet, rassemblant l’ensemble des informations concernant un membre du club.

Le type exact de la table associative sera donc Map<String, Membre> et, le moment venu, vous devrez faire une déclaration+initialisation de la forme

    private Map<String, Membre> registre = new TreeMap<String, Membre>(); 

N.B. 1. Si vous utilisez une version de Java ≥ 7 (c’est-à-dire JDK ≥ 1.7) vous pouvez écrire la ligne précédente sous la forme plus légère :

private Map<String, Membre> registre = new TreeMap<>(); 

N.B. 2. Pour implémenter l’interface Map vous pouvez utiliser HashMap au lieu de TreeMap, mais c’est probablement une fausse bonne idée : la recherche d’un couple (clé, valeur) est un peu plus efficiente dans une HashMap, mais une TreeMap possède l’avantage que lors d’un parcours des clés celles-ci apparaissent triées dans leur ordre naturel.

Pour avoir constamment une vue sur les données couramment possédées (et à défaut d’une présentation plus savante, genre JTable, qui sortirait du propos de cet exercice) on affichera celles-ci dans une zone de texte occupant la plus grande partie du cadre principal de l'application :

A. L’application qui utilisera la boîte de saisie

Pour commencer, mettez de côté l'application CadreSaisieMembre réalisée à la première question et repartez de zéro : définissez une nouvelle application ClubSportif réduite à un cadre principal (JFrame) ayant :

Pour créer une zone de texte, vous savez déjà faire. Pour la création des menus on écrira une méthode JMenuBar barreDeMenus() ayant la structure générale suivante :

    JMenuBar barreDeMenus() {
        JMenuBar barre = new JMenuBar();
        ...
        return barre;
    }

Elle est destinée à être appelée dans le constructeur du cadre principal, par une expression de la forme

...
this.setJMenuBar(barreDeMenus());
...

Entre la première et la dernière ligne de la fonction barreDeMenus il faudra créer et rattacher les divers menus souhaités (pour commencer, un seul menu) :

        ...
        JMenu menu = new JMenu("Actions");
        barre.add(menu);
        ...

À chaque menu sont rattachés plusieurs items de menu. Chacun est associé à un ActionListener anonyme dont la méthode actionPerformed consiste en l’appel d’une méthode spécifique dont le nom rappelle le texte de l'item de menu (on aura donc les méthodes creationMembre, modificationMembre, suppressionMembre, etc.), selon le schéma :

        ...
        JMenuItem item = new JMenuItem("Modification membre");
        menu.add(item);
        item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                modificationMembre();
            }
        });
        ...

Pour commencer, les méthodes creationMembre, modificationMembre, etc., se limitent à afficher une boîte d’excuse (utilisez les méthodes prêtes à l’emploi showXXXdialog de la classe JOptionPane) :

B. Quitter le programme

On fera en sorte que la case de fermeture du cadre (et la commande Quitter du menu) appellent une méthode quitter() dans laquelle nous mettrons plus tard du code pour demander à l’utilisateur de confirmer sa volonté de quitter le programme. Pour la case de fermeture, cela consistera à remplacer l’habituel

    ...
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    ...

par quelque chose de plus élaboré :

    ...
    setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
    addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent arg0) {
            quitter();
        }
    });
    ...

C. Convertir l’interface de la question 3b.2 en une boîte de dialogue

Définissez une classe DialogueSaisieMembre, sous-classe de JDialog. Un objet JDialog, comme un objet JFrame, est un composant du plus haut niveau (JFrame et JDialog sont des sous-classes de Window)[1] mais, contrairement à JFrame, un JDialog est en principe destiné à apparaître n’importe quand et à disparaître aussi soudainement.

Cela commence ainsi :

public class DialogueSaisieMembre extends JDialog {

    public DialogueSaisieMembre(JFrame cadre, String titre) {
        super(cadre, titre, true);
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        setContentPane(panneauDeContenu());
        pack();
    }
    ...
}

Dans super(cadre, titre, true), un appel du constructeur de JDialog, l’argument cadre représente le cadre principal de l’application, qui est la fenêtre « propriétaire » (owner) de la boîte de dialogue ; titre est le titre de la boîte de dialogue et true indique qu’on souhaite que cette boîte de dialogue soit modale : aussi longtemps qu’elle sera visible, elle bloquera l’exécution de l’application qui l’a fait apparaître.

La méthode panneauDeContenu() manque : transportez ici (copier-coller) celle que vous avez écrite pour la classe CadreSaisieMembre, cela complète le constructeur de DialogueSaisieMembre. Ensuite, modifiez ainsi la méthode creationMembre du cadre principal :

    ...
    private void creationMembre() {
        DialogueSaisieMembre dial = new DialogueSaisieMembre(this, "Création membre");
        dial.setLocationRelativeTo(this);
        dial.setVisible(true);

        System.out.println("Dialogue terminé");
    }
    ...

Essayez l’application et vérifiez que l’actionnement de la commande Création membre du menu Actions provoque l’apparition de la boîte de dialogue et le blocage de l’application.

Les boutons OK et Annuler étant encore sans effet, fermez la boîte de dialogue en agissant sur la case de fermeture du cadre. Constatez que le message Dialogue terminé apparaît alors.

D. La fin du dialogue

Rendez les boutons OK et Annuler actifs en leur associant des ActionListener anonymes qui se réduisent à des appels de méthodes correspondantes nommées, par exemple, actionBoutonAccepter et actionBoutonAnnuler.

La méthode actionBoutonAnnuler est très simple :

    private void actionBoutonAnnuler() {
        dispose();
    }

Notez que, puisque dans le constructeur du dialogue nous avons spécifié setDefaultCloseOperation(DISPOSE_ON_CLOSE), l’effet du bouton Annuler et celui de la case de fermeture du dialogue sont identiques.

La méthode actionBoutonAccepter est un peu plus complexe : elle doit provoquer également la disparition de la boîte de dialogue, mais uniquement après avoir vérifié que les données saisies sont valides (c’est-à-dire que toutes les informations obligatoires ont été données et que les valeurs fournies vérifient les éventuelles contraintes qui leur sont imposées). Si des informations sont manquantes ou incorrectes, des messages doivent être affichés et la boîte de dialogue doit rester présente. Si les informations sont valides, la boite de dialogue doit disparaître.

Les actions sur les boutons Annuler ou OK (lorsque les données sont valides) produisent donc la disparition de la boîte de dialogue. Notez bien que cela signifie la destruction de l’entité graphique (créée lors de l’appel de setVisible(true)) mais non de l’objet Java sous-jacent qui, lui, ne sera détruit (par le garbage collector) que lorsqu’il ne sera plus référencé par aucune variable accessible.

D’autre part il est nécessaire que l’objet qui a provoqué l’affichage du dialogue puisse savoir si celui-ci s’est terminé par une acceptation suivie d’une validation des données (bouton OK), ou bien par un abandon (bouton Annuler ou case de fermeture), auquel cas les données n’ont pas été acquises. A cet effet ajoutez à la classe DialogueSaisieMembre une variable booléenne, par exemple private boolean bonneFin, et accesseur public boolean getBonneFin(). La variable bonneFin est initialisée à false et ne devient true que lorsque le bouton OK a a été actionné et que les données sont valides.

En supposant que la validation des données est effectuée par une méthode

    private boolean donneesValides() {
        return true;  /* version provisoire */
    }

écrivez les méthodes actionBoutonAnnuler et actionBoutonAccepter et constatez que le dialogue apparaît et disparaît correctement.

E. Récupérer les informations saisies...

Pour permettre à l’objet qui a provoqué l’affichage du dialogue de récupérer les informations que l’utilisateur y a saisies il faut déjà qu’on puisse accéder aux composants concernés en dehors du constructeur qui les a créés et installés. Un premier petit travail à faire sur notre classe DialogueSaisieMembre consiste donc à faire en sorte que les composants contenant des informations soient représentés par des variables d’instance (et non qu’ils soient anonymes ou représentés par des variables locales).

Si elles n’existent pas déjà dans votre classe, introduisez les variables d’instance privées suivantes (les noms sont donnés à titre d’exemple)  :

Une fois que ces composants auront été ainsi représentés par des variables d’instance, vous pourrez écrire une méthode donneesValides() plus consistante que le garde-place donné ci-dessus. Par exemple, vous pourriez vérifier que

Ce qui précède concerne plutôt les détails internes. Vis-à-vis de l’extérieur vous allez faire en sorte que votre boîte de dialogue respecte l’esprit des JavaBeans[2], ce qui consiste essentiellement à posséder un certain nombre de propriétés[3]. Dans votre cas, vous ferez en sorte que votre DialogueSaisieMembre possède les propriétés – en lecture et écriture – suivantes :

Pour définir ces propriétés vous devrez écrire des couples de méthodes get et set qui dans certains cas (nom, prenom...) se réduiront à de simples consultations ou affectations de variables mais qui, dans d’autres cas (sportsChoisis) seront plus complexes.

F. Et les ranger quelque part

Définissez une classe Membre pour représenter ces informations. Pas besoin de faire compliqué, ce n’est qu’une classe passive (semblable à une struct du langage C) pour garder ensemble ce qui concerne une personne : donnez-lui les cinq champs publics listés ci-dessus et un constructeur banal qui prend pour arguments ces cinq informations.

On rangera les informations saisies dans une table associative (structure de données Map) nommée par exemple registre, composée de couples (clé, valeur) où clé est une chaîne de caractères composée avec le nom et le prénom du membre et valeur est un objet Membre.

Devant servir à des recherches, il y a intérêt à ce que les clés soient normalisées. A cet effet, écrivez une méthode utilitaire

private String cle(String nom, String prenom)

Cette méthode « rogne » (enlève les blancs au début et à la fin, il y a une méthode pour ça dans la classe String) les chaînes nom et prenom, les concatène avec un blanc entre les deux puis met le tout en minuscules (il y a également une méthode pour ça dans String).

Avant de ranger un couple (clé, valeur) nouvellement saisi on vérifiera qu’un couple ayant même clé n’existe pas déjà dans le registre. Si c’est le cas on affichera un message et on ne modifiera pas le registre.

Enfin, écrivez une méthode private void affichageRegistre() qui :

Cette méthode sera appelée après chaque création (et, plus tard, modification et suppression) réussie pour permettre de suivre l’évolution du registre et le bon fonctionnement du programme.

3b.4  Modifier et supprimer des informations

Il reste à s’occuper des méthodes modificationMembre et suppressionMembre qui, jusqu’à présent, ne font qu’afficher un panneau d’excuse.

Pour commencer, ces deux méthodes doivent demander la clé, c’est-à-dire le nom et le prénom du membre à modifier ou supprimer. Pour ne pas avoir à construire de toutes pièces une nouvelle boîte de dialogue, nous allons nous contenter d’une boîte de saisie prête à l’emploi, celle qu’on obtient par la méthode showInputDialog de la classe JOptionPane. L’utilisateur doit donner le nom et le prénom du membre visé séparés par une virgule :

S’il oublie la virgule, un message le lui indique immédiatement

puis la première boite réapparaît. Vous devez alors extraire de la chaîne saisie le nom et le prénom, fabriquer la clé correspondante (méthode cle, voir plus haut) et chercher le membre dans le registre. S’il n’existe pas, afficher un message d’erreur. Sinon, dans le cas de la suppression, demander une confirmation puis supprimer la paire (clé, valeur) du registre.

Dans le cas d’une modification il faut afficher la boîte de saisie et la laisser faire, avec deux nouveautés par rapport à ce que nous avons fait à l’exercice précédent :

La fin du travail (extraction des informations de la boîte de dialogue et rangement dans le registre) est la même que dans le cas d’une création.

3b.5  Sauvegarder et restaurer les données

Pour commencer, ajoutez à votre application un menu Fichier avec les commandes Ouvrir... et Enregistrer sous... Dans la foulée, déplacez dans ce menu la commande Quitter (présentement dans le menu Actions), qui y sera plus à propos. Il nous reste à écrire les deux méthodes associées au nouveau menu : menuFichierEnregistrer et menuFichierOuvrir. Cela nous fera une petite visite guidée des deux classes les plus puissantes pour gérer des fichiers en Java : ObjectOutputFile et ObjetInputFile.

A. Sauvegarde

1. La méthode menuFichierEnregistrer doit enregistrer dans un fichier la totalité des informations contenues dans la Map que nous avons appelé registre. Cela nous fera utiliser trois classes liées aux fichiers, appartenant au paquetage java.io :

Si nous voyons les flux comme des tuyaux, il s’agira de connecter (comme le ferait un plombier) un ObjectOutputStream à un FileOutputStream, nous obtiendrons alors un tuyau qui par une extrémité prend des objets Java et par l’autre extrémité insère des octets dans un fichier.

De manière symétrique, dans la méthode menuFichierOuvrir nous aurons besoin de File et de deux autres classes :

En « branchant » un ObjectInputStream à un FileInputSTream nous obtiendrons un tuyau qui par une extrémité prend des octets dans un fichier et par l’autre extrémité produit des objets Java.

2. Que ce soit pour le lire (cas de menuFichierOuvrir) ou pour l’écrire (cas de menuFichierEnregistrer) il faudra commencer par obtenir le nom du fichier. Cela se fait avec un dialogue prêt à l’emploi, instance de la classe JFileChooser, dont on provoque l’affichage à travers une des méthodes showOpenDialog (choix d’un fichier en lecture) ou showSaveDialog (détermination du nom d’un fichier en écriture). Il s’agit d’une boite de dialogue modale : ces deux méthodes renvoient un entier qui indique si le dialogue s’est bien (valeur JFileChooser.APPROVE_OPTION) ou mal (autres valeurs) terminé. Si le dialogue s’est bien terminé, la méthode getSelectedFile() renvoie l’objet File que le dialogue a permis de désigner.

Une fois le fichier désigné, il ne reste plus qu’à monter la tuyauterie indiquée plus haut, puis à enregistrer dans le fichier la valeur de la variable registre. Cela s’écrit :

            ...
            FileOutputStream fos = new FileOutputStream(fichier);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            
            oos.writeObject(registre);
            
            fos.close();
            ...

3. La méthode writeObject de la classe ObjectOutputStream est donc capable de transformer en une suite d’octets (cela s’appelle sérialiser) n’importe quel objet. Or un objet peut être extrêmement compliqué[4], on voit donc que cette fonction merveilleuse effectue un travail complexe et précieux.

Sérialiser un objet c’est le mettre en conserve dans un fichier, probablement pour le ressortir plus tard. La méthode writeObject peut faire cela pour n’importe quel objet, mais il peut y avoir des situations où la sérialisation est erronée ou dangereuse : les objets peuvent comporter des éléments, dits transitoires (transient), qui perdent leur sens ou deviennent incorrects lorsque le temps passe ou que le contexte change. C’est la raison pour laquelle Java demande au programmeur d’indiquer, quand il écrit une classe, qu’il autorise la sérialisation de ses instances. Cela se fait par un énoncé tel que :

public class Membre implements Serializable {
    ...
}

Cette expression n’entraîne aucune corvée, l’interface Serializable (du paquetage java.io) est vide! On dit que c’est une interface de marquage : le programmeur ajoute l’expression « implements Serializable » uniquement pour marquer que la sérialisation des instances de cette classe est permise.

Les structures de données de la bibliothèque, dont les Map, sont sérialisables. Or, les objets référencés par un objet sérialisable doivent être sérialisables à leur tour pour que la sérialisation soit possible. Comme registre est faite de couples (clé, valeur), avec clé de type String (une classe sérialisable) et valeur de type Membre, la déclaration ci-dessus est nécessaire pour que l’opération « oos.writeObject(registre); » soit possible.

4. Les méthodes de traitement de fichiers (les créations d’objets Stream, les opérations de lecture et écriture, même les clôtures de flux) sont génératrices d’exceptions contrôlées[5]. Cela vous obligera à écrire les quatre lignes précédemment montrées sous la forme

        ...
        try {
            FileOutputStream fos = new FileOutputStream(fichier);
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            oos.writeObject(registre);
            
            fos.close();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(this, e.toString(),
                    "Erreur fichier", JOptionPane.ERROR_MESSAGE);
        }
        ...

B. Restauration

1. La méthode menuFichierOuvrir est, d’une certaine manière, symétrique de menuFichierEnregistrer. On vous laisse faire...

2. Maintenant que la restauration et la sauvegarde des données fonctionnent, vous pouvez écrire la version définitive de la méthode quitter. A cet effet, ajoutez à l’application ClubSportif une variable booléenne donneesSauvees qui est vraie lorsque registre est vide ou identique à une version sauvée dans un fichier, fausse autrement.

Diverses actions du programme changement la valeur de cette variable : création, modification, suppression de membres, enregistrement du registre, etc. La méthode quitter (activée par la commande Quitter du menu Fichier ou par la case de fermeture du cadre principal) doit tester cette variable et, si elle est fausse, refuser de quitter ou demander confirmation.

3b.6  Utiliser WindowBuilder

Les applications usuelles contiennent de très nombreuses boîtes de dialogue. Or, comme on vient de le voir, construire une boîte de dialogue est une tâche longue et ennuyeuse. Heureusement, la plupart des environnements de développement intégrés (eclipse, NetBeans, etc.) offrent une aide considérable : la possibilité de composer les interfaces graphiques à l’aide de la souris, en déposant sur une interface en cours de construction, mais constamment correcte et active, des composants prêts à l’emploi pris dans des réservoirs de composants appelés palettes.

L’outil de cette sorte inclus dans eclipse – depuis la version Indigo ou eclipse 3.7 – s’appelle WindowBuilder Pro. Pour en faire une visite guidée, nous allons l'utiliser pour construire de nouveau, à partir de zéro, la boîte de dialogue de l’application précédente, étant bien entendu que

La documentation officielle sur WindowBuilder peut être consultée en ligne (dans le panneau Contents recherchez l’entrée WindowBuilder Pro User Guide) ou téléchargée (suivez les instructions).

Le principe de WindowBuilder est de maintenir constamment deux vues sur la classe en cours de développement :

L’interface en construction montrée dans la vue Design ressemble beaucoup à l’interface qu’on obtiendra lors des exécutions ultérieures mais ne lui est pas tout à fait identique. Cela n’a pas d'importance, car l’application est constamment compilée et peut être exécutée à tout moment, montrant l’aspect exact qu’aura l’interface.

Une autre qualité de WindowBuilder est qu’il est parfaitement bi-directionnel : vos actions « à la souris » sur l’interface modifient le code source ; inversement, si vous agissez sur le code source, l’interface en sera immédiatement modifiée en conséquence.

A. Démarrage

Lancez eclipse et créez un paquetage destiné à contenir les classes que vous allez réaliser. Exécutez la commande File > New > Other...

Apparaît un panneau Select a wizard, ouvrez WindowBuilder puis SwingDesigner enfin (puisque vous voulons créer une boîte de dialogue) JDialog :

Pressez le bouton Next, apparaît une boîte demandant le nom de la classe. À l’instar des exercices précédents, saisissez DialogueSaisieMembre. Pressez le bouton Finish, apparaît le code source d’une classe que vous pouvez examiner (il est tout à fait lisible) ou exécuter, ce qui fait apparaître ceci :

C’est bien une boîte de dialogue, pour le moment très rudimentaire (seule la case de fermeture fonctionne).

Dans eclipse, affichez le volet Design, l’environnement change d’aspect (pour gagner de la place nous l’avons débarrassé des panneaux latéraux, Package Explorer, etc.) :

Repérez les quatre parties principales de cette interface (voyez les images ci-dessus et ci-dessous)  :

Attention, double-cliquer sur un composant, dans la vue de conception ou dans l’arbre des composants, ne fait pas que le sélectionner, pour certains composants cela provoque une modification du code. Par exemple, double-cliquer sur un bouton ajoute au code l’ActionListener correspondant.

B. Premières manipulations

Pour nous faire la main, proposons nous d’obtenir que dans la boite de dialogue rudimentaire que nous avons déjà les deux boutons OK et Cancel :

  1. affichent OK et Annuler,
  2. soient au milieu,
  3. soient de même taille.

1. Dans la vue de conception, cliquez une fois sur le bouton Cancel. Dans le panneau des propriétés, recherchez la propriété text et changez-en la valeur.

2. Dans la vue de conception, cliquez une fois sur le panneau gris contenant les deux boutons. Dans le panneau des propriétés, recherchez la propriété Layout. Constatez que sa valeur (java.awt.FlowLayout) est correcte ; déroulez les propriétés de cette propriété (petit signe + à gauche du mot Layout) et trouvez alignment. Cliquez sur RIGHT et changez-le en CENTER.

3. Manière rapide. Dans la vue de conception, cliquez une fois sur le bouton Annuler. Dans le panneau des propriétés cherchez la propriété preferredSize (c’est une propriété avancée), observez sa valeur (par exemple (77,25)). Dans la vue de conception, cliquez une fois sur le bouton OK et donnez à la propriété preferredSize cette même valeur.

Manière moins rapide. Affichez le code (volet Source) pour voir comment les boutons sont représentés :

            ...
            JPanel buttonPane = new JPanel();
            buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
            getContentPane().add(buttonPane, BorderLayout.SOUTH);
            {
                JButton okButton = new JButton("OK");
                okButton.setActionCommand("OK");
                buttonPane.add(okButton);
                getRootPane().setDefaultButton(okButton);
            }
            {
                JButton cancelButton = new JButton("Cancel");
                cancelButton.setActionCommand("Cancel");
                buttonPane.add(cancelButton);
            }
            ...

Zut ! okButton et cancelButton sont des variables locales de blocs disjoints, cela empêche d’avoir les deux à la fois. Sélectionnez okButton, exécutez la commande Refactor > Convert Local Variable to Field... et acceptez les options par défaut. Faites la même chose avec cancelButton. Ensuite, écrivez immédiatement après les lignes précédentes (qui ont dû être légèrement modifiées) :

            okButton.setPreferredSize(cancelButton.getPreferredSize());

Exécutez le programme et constatez que les boutons ont maintenant l’aspect souhaité :

C. Les « grands » panneaux

Tout au long de cette section et ses suivantes vous avez intérêt à vous rappeler les schémas de la question 3b.2.

Sélectionnez (cliquez une fois) le panneau de contenu, c.-à-d. celui qui ne porte pas les deux boutons (dans l’arbre de contenu il s’appelle contentPane). Recherchez sa propriété Layout (actuellement elle vaut java.awt.FlowLayout) et, à l’aide du petit menu déroulant, donnez-lui la valeur java.awt.BorderLayout.

Dans la palette Containers sélectionnez JPanel. Amenez le curseur au-dessus du panneau de contenu et remarquez l’aspect qu’il prend pour vous rappeler qu’il est à présent géré par un BorderLayout :

Cliquez au centre : tac !, vous venez d’ajouter un nouveau panneau dans le panneau de contenu, avec la contrainte BorderLayout.CENTER. Sélectionnez ce nouveau panneau et donnez à sa propriété Variable la valeur grandCentre.

Refaites ces opérations, pour ajouter un nouveau panneau avec la contrainte BorderLayout.EAST. Donnez à sa propriété Variable la valeur grandEst.

D. Les « petits » panneaux

Sélectionnez le panneau que vous avez ajouté au milieu (il s’appelle grandCentre) et modifiez sa propriété Layout pour qu’elle ait la valeur java.awt.BorderLayout.

Mettez dans ce panneau Panel trois nouveaux panneaux avec les contraintes BorderLayout.NORTH, BorderLayout.CENTER et BorderLayout.SOUTH, respectivement. Donnez à la propriété Variable de ces panneaux les valeurs petitHaut, petitCentre, petitBas, respectivement.

Comme on empile des panneaux vides, il est possible que la sélection dans la vue de conception soit difficile ou impossible. Cela ne fait rien, vous pouvez toujours ajouter les panneaux en agissant sur l’arbre des composants et en vérifiant, sur le panneau des propriétés, que la contrainte est celle qu'il faut :

E. Le club des cinq

Sélectionnez petitNord, le premier des petits panneaux. Changez sa propriété Layout pour lui donner la valeur java.awt.GridLayout. Déroulez les propriétés de cette propriété (petit signe + à gauche du mot Layout) et donnez les valeurs 1 à la propriété columns et 0 à la propriété rows.

Dans la palette Components, sélectionnez le composant JLabel et cliquez sur petitNord, dans la vue de conception ou (plus facile) dans l’arbre des composants. Une fois le label créé, donnez la valeur Nom a sa propriété text.

Dans la palette Components, sélectionnez le composant JTextField et cliquez dans le panneau. Une fois le champ de texte créé, donnez la valeur ChampNom à sa propriété Variable.

Refaites les deux opérations précédentes, avec Prénom et champPrenom à la place de Nom et champNom.

Ajoutez encore un JLabel avec le texte Adresse. La vue de conception devrait maintenant ressembler à ceci :

F. La zone de texte

Faites en sorte que le second petit panneau (petitCentre) ait pour layout java.awt.BorderLayout.

Dans la palette Containers, sélectionnez JScrollPane et cliquez au centre de ce second petit panneau. Ensuite, dans la palette Components, sélectionnez JTextaArea et cliquez dans le JScrollPane (secteur Viewport) que vous venez de placer. Donnez à la propriété Variable de ce JTextArea la valeur zoneAdresse.

Maintenant que tous les composants de saisie de texte sont placés, vous pouvez regarder si vous kiffez la police de caractères qu’ils utilisent et, le cas échéant, la changer (propriété font) ; dans notre cas, nous avons choisi Courier New, Plain, 16.

Si l’interface visible dans la vue de conception vous semble bizarre, n’oubliez pas que vous pouvez à tout moment exécuter l’application, vous aurez alors l’apparence exacte :

(mettre une adresse exagérement longue nous permet de constater que le scroll pane fonctionne).

G. Du sexe

Sélectionnez le troisième petit panneau (petitSud) et vérifiez que sa propriété Layout vaut jawa.awt.FlowLayout. En les sélectionnant dans la palette Components puis en cliquant dans ce panneau, mettez-y successivement un JLabel et deux JRadioButton. En agissant sur leurs propriétés text faites en sorte qu’ils affichent Sexe, Homme et Femme et que la propriété Variable des boutons radio vaille boutonHomme et boutonFemme respectivement.

Cliquez avec le bouton droit de la souris sur le premier bouton radio. Dans le menu contextuel qui apparaît, choisissez Set ButtonGroup et, dans un sous-menu, New Standard. Cela crée un groupe nommé (bêtement) buttonGroup, visible tout en bas de l’arbre des composants.

Cliquez avec le bouton droit de la souris sur le second bouton radio. Dans le menu contextuel, choisissez Set ButtonGroup puis buttonGroup (le groupe que vous venez de créer en agissant sur l’autre bouton).

Donnez à la propriété selected de l’un des deux boutons radio la valeur true. Exécutez l’application et constatez que les deux boutons radio fonctionnent ensemble, c'est-à-dire sont mutuellement exclusifs :

H. Les sports

Nous atteignons maintenant les limites du design graphique bi-directionnel. En effet, nous souhaitons, comme dans les exercices précédents, représenter les sports pratiqués par un membre à l’aide d’un tableau de JCheckBox, cela est simple, astucieux et nécessaire pour récupérer telles quelles les méthodes void Set<Sports> getSportsChoisis() et void setSportsChoisis(Set<Sports> s) déjà écrites. Or, il semble que WindowBuilder ne connaît pas les tableaux et ne tient pas une expression comme « casesACocherSports[i] » pour une variable. Il va donc falloir ruser pour obtenir ce que nous voulons.

Pour commencer, allez dans le volet Source et parmi les déclarations des variables d’instance de DialogueSaisieMembre ajoutez :

private JCheckBox[] casesACocherSports = new JCheckBox[Sports.NOMBRE];

Revenez au volet Design. Dans la palette Components sélectionnez le composant jCheckBox et cliquez dans le panneau grandEst, ce qui ajoute une case à cocher étiquetée New Check Box. Sélectionnez cette case à cocher et allez dans le volet Code. Vers le milieu de la fenêtre vous devriez apercevoir les lignes :

...
{
    JCheckBox chckbxNewCheckBox = new JCheckBox("New check box");
    grandEst.add(chckbxNewCheckBox);
}
...

Modifiez ce bout de code comme ceci :

...
int i = 0;
{
    JCheckBox chckbxNewCheckBox = casesACocherSports[i] = new JCheckBox(Sports.values()[i].toString());
    i++;
    grandEst.add(chckbxNewCheckBox);
}
...

Maintenant ajoutez 8 copies du texte délimité par les accolade (sélectionnez de ‘{’ à ‘}’ et faites copier suivi de 9 fois coller) :

...
int i = 0;
{
    JCheckBox chckbxNewCheckBox = casesACocherSports[i] = new JCheckBox(Sports.values()[i].toString());
    i++;
    grandEst.add(chckbxNewCheckBox);
}{
    JCheckBox chckbxNewCheckBox = casesACocherSports[i] = new JCheckBox(Sports.values()[i].toString());
    i++;
    grandEst.add(chckbxNewCheckBox);
}{
    JCheckBox chckbxNewCheckBox = casesACocherSports[i] = new JCheckBox(Sports.values()[i].toString());
    i++;
    grandEst.add(chckbxNewCheckBox);
}
    etc.

La vue de conception est devenue :

I. La fin

Dans la vue de conception, double-cliquez sur le bouton OK et constatez que cela génère un ActionListener pour ce bouton, qu’il suffira de compléter plus tard. Faites de même sur le bouton Annuler.

Dans l’arbre des composants sélectionnez la racine (étiquetée (javax.swing.JDialog)), c’est-à-dire le cadre principal de la boîte de dialogue. Constatez qu’en agissant sur ses poignées de dimensionnement vous pouvez donner à ce cadre principal la taille et la forme qui vous conviennent.

Profitez-en pour donner un titre à ce cadre principal (propriété title).

Vérifiez que les composants contenant des informations sont représentés par des variables d’instance (non des variables locales) qui ont les mêmes noms que dans l’exercice 3b.3 : champNom, champPrenom, zoneAdresse, boutonHomme, boutonFemme, casesACocherSports, auxquelles il faut ajouter bonneFin. Au besoin, utilisez les commandes Refactor > Convert Local Variable to Field et Refactor > Rename... pour obtenir cette configuration.

Il ne vous reste plus qu’à récupérer (copier-coller, vous connaissez ?) des méthodes de la classe réalisée à l’exercice 3b.3 pour compléter la classe DialogueSaisieMembre.

Pour obtenir la classe principale ClubSportif vous pouvez dans un premier temps récupérer celle qui a été précédemment écrite.

Dans un deuxième temps, complétez votre visite guidée en repartant de zéro et en utilisant WindowBuilder pour créer le cadre principal et ses menus.

La suite des TP...

[1] Du plus haut niveau signifie que les Window sont les composants les plus extérieurs : un objet Window contient les autres éléments de l’interface mais, lui, n’est contenu dans aucun autre composant. En particulier, c’est lui qui gère les interactions avec le système d’exploitation et les autres applications qui apparaissent sur le même écran graphique. [retour]

[2] Ce n’est pas le lieu ici de faire un exposé sur les JavaBeans (si la question vous intéresse, voyez par exemple Présentation de la technologie JavaBeans). Sachez que cela consiste surtout à posséder un certain nombre de propriétés (voir la note suivante). Sachez aussi que tous les composants graphiques prédéfinis respectent ce principe et sont donc des JavaBeans, et que c’est cela qui permet des les avoir dans des palettes et de les composer « à la souris », comme nous verrons plus loin. [retour]

[3] On dit qu’un composant a une propriété nommée prop lorsqu’il exhibe les deux méthodes :

On dit que type est le type de la propriété. Si seule la méthode getProp existe on dit que la propriété est en lecture seule. [retour]

[4] Un objet contient en général d’autres objets. Comme les objets sont désignés par référence, il en résulte qu’un objet est en général un graphe orienté, parfois cyclique, rarement un arbre. Bref, une structure bien difficile à parcourir et encore plus difficile à reconstruire. Heureusement, les flux d’objets le font pour nous ! [retour]

[5] Une exception est un objet qui notifie la survenue d’une situation anormale rendant impossible la poursuite de l’exécution. Parlant simplement, une exception est le signalement d’une erreur à l’exécution.

Toutes les exceptions sont instances de sous-classes de Throwable. Celles qui ne sont pas instances de sous-classes de Error ou de RuntimeException sont dites contrôlées. Cela signifie que le programmeur doit obligatoirement soit les déclarer soit les attraper.

Imaginez qu’uneMethode contienne un morceau de code, appelons-le codeExplosif, susceptible de lancer une exception (contrôlée) de type BoumException. Déclarer cette exception c’est écrire la méthode comme ceci :

void uneMethode( arguments ) throws BoumException {
    ...
    codeExplosif
    ...
}

Cette déclaration indique que l’appel de uneMethode est susceptible de laisser échapper une exception de type BoumException et devenir ainsi aussi « explosif » que les lignes codeExplosif à l’origine du problème.

Au contraire, attraper l’exception c’est écrire la méthode sous la forme :

void uneMethode( arguments ) {
    ...
    try {
        codeExplosif
    } catch (BoumException be) {
        code qui sera exécuté si l’exception be de type BoumException est lancée dans codeExplosif
    }
    ...

}

dans ce cas, l’exception be lancée par codeExplosif ne « sort » pas de la fonction, elle termine sa carrière dans la clause catch. [retour]