Démineur - correction phase I

Introduction

Le but de ce TP étendu sur plusieurs séances est de réaliser un jeu graphique, le"démineur" dont les règles sont présentées dans la section suivante.
Il existe plusieur façons de réaliser ce jeu, nous allons exploiter les composants de Swing pour assurer l'affichage et la récupération des événements utilisateur.

Exercice D.0 Pour réaliser ce TP vous devrez créer un nouveau répertoire DEMINEUR, et y copier l'ensemble du répertoire RESGRAF que vous trouverez à l'adresse : ~garreta/RessourcesDemineur/

Ce répertoire contient l'ensemble des ressources graphiques nécessaires au jeu, et notamment les thèmes SMILEY, INSECTE, SKI et HALLOWEEN placés dans des sous-répertoires de même nom, et qui servent à personnaliser l'apparence du jeu.

ATTENTION : ces thèmes comportent certaines images au format gif qui ne sont pas libres de droits, vous pouvez les garder pour votre usage personnel, mais ne vous en servez pas pour illustrer vos pages web par exemple.

Règles du jeu

L'image ci-dessous présente l'application telle qu'elle apparaît en cours de jeu, le thème courant étant SMILEY. La fenêtre comporte une barre de contrôle d'action, constituée par une barre d'outils qui comporte trois icônes. Chaque icône est en fait un bouton qui permet de sélectionner le mode d'action courant :


La fenêtre comporte également une grille de taille variable, qu'il va falloir explorer, ainsi qu'une barre de menus que nous étudierons le moment venu, elle permettra de fixer la taille de la grille, le niveau de difficulté, et le thème graphique courant.
La case en haut et à gauche de la grille est assurée de ne pas comporter de mine.
 

Les règles du jeu sont simples, le but est d'explorer l'ensemble de la grille sans marcher sur les cases minées, ceci en s'aidant des informations d'adjacences affichées à chaque fois que l'on explore une case en marchant dessus, sauf dans le cas où l'on marche sur une case minée bien sûr ! L'information d'adjacences d'une case indique le nombre de mines qui se trouvent dans l'ensemble des huit cases qui l'entourent. Le concept est décrit dans l'image suivante sur une grille 3x3 :

En (a) la case du centre a été explorée, elle révèle un total de 2 cases adjacentes minées. La figure (b) présente les 9 cases révélées. Elle fait apparaître les deux mines, et les chiffres qui caractérisent l'information d'adjacences pour chacune des 7 cases non minées.
Lorsqu'une grille a été complétée (totalement déminée), le jeu se poursuit avec une nouvelle grille. Le jeu se termine lorsque le joueur marche sur une mine.

Indications spéciales concernant le développement

IMPORTANT : l'application finale comporte un certain nombre de classes séparées que nous développerons au fur et à mesure des TPs. Afin que toutes fonctionnent correctement ensemble suivez absolument les instructions qui vous seront données pour la création de chaque classe dans l'ordre où elles vous sont présentées.

Attention : afin de vous éviter de taper inutilement du code et pour que vous puissiez vous concentrer sur les parties importantes, certaines méthodes vous sont intégralement fournies, mais le rôle de chacune sera considérée comme acquis par la suite.
 

Partie 1 : La fenêtre de base

Pour réaliser l'application nous allons avoir besoin d'un cadre. Créez dans le répertoire DEMINEUR un fichier intitulé FenetreSimple.java qui définit la classe FenetreSimple ci-dessous :

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

public class FenetreSimple extends JFrame{
 public FenetreSimple(String p_titre)
 {
  super(p_titre);

  addWindowListener(new WindowAdapter()
   {
           public void windowClosing(WindowEvent e) {
    System.exit(0);
   }
  });
  setSize(200,100);
 }

 public static void main(String args[])
 { 
  FenetreSimple cadre = new FenetreSimple("Une fenetre simple");
  setVisible(true);
 }
}

Note : pour que vos programmes Java comportant des composants Swing fonctionnent correctement avec les threads (un thread peut être vu comme un processus en C, ou une tâche en Ada), il y une règle importante à respecter :

lorsqu'un composant a été affiché, tout code qui affecte l'état de ce composant ou qui en dépend doit être effectué dans le thread qui a en charge la gestion des événements (typiquement dans une méthode actionPerformed ou équivalente, selon le cas).

Ceci signifie qu'un thread ne doit pas directement ajouter un composant à l'interface d'un autre thread.

Pour éviter de créer des applications qui peuvent poser des problèmes suivez dans vos programmes le schéma suivant :
 

public class MonApplication {
    public static void main(String[] args) {
 JFrame f = new JFrame(...);
        ...//Add components to the frame here...
 f.pack();
 f.setVisible(true);
 //Après le pack et l'affichage, l'interface ne doit plus être directement modifiée.
    }

    ...
    // Toutes les opérations sur les composants de l'interface -setText,
    // getText, etc.-
sont ensuite effectuées dans les gestionnaires
    // d'événements tels que actionPerformed().

    ...
}

Plus généralement, celà revient à dire que tous les composants d'un cadre doivent être mis en place avant l'affichage du cadre, et qu'ensuite, toutes les modifications ou consultations de l'état de ces composants doit être effectuée en réponse à des événements utilisateur.
Bien sûr il peut y avoir y avoir des exceptions, ainsi il peut parfois être utile d'effectuer ces opérations liées aux composants dans d'autres threads que le thread chargé de la gestion des événements. La classe SwingUtilities fournit deux méthodes dans ce but :
invokeLater (Runnable code) : qui provoque l'execution de code dans le thread chargé des événements. Cette méthode retourne immédiatement, sans attendre que le code soit éxécuté.
invokeAndWait(Runnable code) : qui fonctionne comme la méthode précédente, mais qui attend la fin d'éxécution de code.

En conséquence, le pack() et le setVisible(true) de votre cadre devraient être les dernières instructions en rapport avec l'interface dans votre main(...).

Partie 2 : Création des thèmes

Le jeu comporte un thème qui définit l'apparence des éléments graphiques qui le composent. Ce thème pourra être modifié avant chaque nouvelle partie.

Créez un nouveau fichier DemineurTheme.javaqui définit une classe publique DemineurTheme.

Un thème n'est rien d'autre qu'un ensemble d'icônes chargées dans un répertoire particulier, nous allons dans un permier temps mettre en place les différentes constantes et variables de notre classe.

Constantes et variables de la classe DemineurTheme

Les constantes

La barre de controle des actions comporte trois boutons radios. Chacun d'eux est constitué de trois icônes associées aux différents états du bouton, non sélectionné, sélectionné, et la dernière pour la gestion du rollover.

Définissez les trois constantes suivantes dans votre classe :

 public static final int NSELECT = 0;
 public static final int SELECT = 1;
 public static final int ROLL = 2;

elles serviront à accéder à l'icône appropriée du thème à l'aide de méthodes que nous définirons par la suite.
 

Les variables

Les variables concernent essentiellement les différentes icônes qui composent le thème. Nous allons employer une technique assez générale dans ce type de situation où l'on cherche à stocker un ensemble assez important d'images chargées depuis le disque, qui repose sur deux séries de tableaux. La première contiendra les noms de base des fichiers à charger, la seconde les icônes.
 

Nom du thème

Un thème comporte un nom, qui devra être le même que le nom du répertoire qui contient les icônes du thème : les icônes du thème INSECTE seront toutes placées dans le répertoire INSECTE.
Ajoutez donc la ligne suivante dans votre classe :

private String nom_theme; // nom du theme et du repertoire qui le contient
 

Icônes de la barre d'outils

Comme nous l'avons indiqué plus haut, la barre d'outils comporte trois boutons radios, chacun étant défini par trois icônes différentes. Nous allons simplement stocker ces icônes à la suite, dans un tableau de 9 objets ImageIcon que nous déclarons comme suit :

private ImageIcon icones_barre[] = new ImageIcon[9];
 // icones pour boutons de la barre d'outils :
  // 0,1,2 // marche, marque_mine, marque_cool, (version non selectionnes)
  // 3,4,5 // les memes en rouge (selectionnes)
  // 6,7,8 // les memes en bleu (rollover)
 

Icônes de la grille

La grille comporte tout d'abord des icônes destinées à afficher le nombre de mines présentes dans les 8 cases adjacentes à la case courante. Ces icônes sont à nouveau stockées dans un tableau :

 //icones pour l'affichage des nbres d'adjacences
 private ImageIcon icones_chiffres[] = new ImageIcon[9];

Les quatre icônes suivantes représentent une case de la grille dans l'un des états suivants :

private ImageIcon icones_grille[] = new ImageIcon[4];
 // icones de la grille
  // 0,1,2,3  // base, mine, stop, cool
 

Icônes des dialogues

Deux dialogues sont destinés à s'afficher respectivement lorsque le joueur perd ou gagne une série. Chaque dialogue comporte une icône personnalisée :

private ImageIcon icones_dialogues[] = new ImageIcon[2];
 //icones des dialogues
  // 0,1 //perdu, gagne
 

Noms des icônes

Afin de faciliter l'écriture des méthodes destinées à charger toutes ces icônes, nous allons déclarer, pour chaque groupe d'icônes un tableau qui comporte le nom de base de cette icône :

 // noms icones de la barre d'outils :
 private String noms_icones_barre[]={"barre_marche", "barre_mine", "barre_cool"};

 // noms icones de la grille :
 private String noms_icones_grille[]={"base", "mine", "stop", "cool"};

 // noms icones des dialogues :
 private String noms_icones_dialogues[]={"gagne", "perdu"};
 

Charger les icônes de la classe DemineurTheme

Exercice D.1. Ecrivez le corps de la méthode suivante :

private void chargerIcones(String repertoire){
}

Le but de cette méthode est de charger dans les différents tableaux les images des icônes stockées dans des fichiers.
L'argument repertoire sert à spécifier le nom du répertoire qui contient lui-même les sous-répertoires des différents thèmes.
Par exemple dans notre cas le sous-répertoire du thème INSECTE est placé dans le répertoire RESGRAF de l'application. L'appel de la méthode sera donc, par exemple chargerIcones("RESGRAF/"); mais nous n'en sommes pas encore là, suivez bien les instructions suivante pour charger correctement les icônes dans le bon ordre.
Note : toutes les images sont stockées au format gif, n'oubliez pas d'ajouter ".gif" à la fin des noms de base.

Chargement des icônes de la barre d'outils

Chaque bouton de la barre d'outils comporte trois icônes, le nom du fichier s'obtient de façon suivante :
  • icône de base, version bouton non sélectionné : nom_de_base+".gif"
  • icône version bouton sélectionné, icône rouge : nom_de_base+"R.gif"
  • icône destinée au rollover icône bleue : nom_de_base+"B.gif"

  • les noms de base étant stockés dans la table noms_icones_barre[].

    Attention : la table icones_barre[] doit contenir dans l'ordre et à la suite :

    Chargement des icônes de la grille

    Les icônes de la table icones_grille[] doivent être chargées dans l'ordre où leurs noms apparaissent dans noms_icones_grille[].

    Les icônes de la table icones_chiffres[] sont stockées dans l'ordre croissant, de 0 à 8. Le nom de chaque fichier s'obtient tout simplement à l'aide de l'opérateur de concaténation de chaines : repertoire+i+".gif"

    Chargement des icônes des dialogues

    Les icônes de la table icones_dialogues[] doivent être chargées dans l'ordre où leurs noms apparaissent dans noms_icones_dialogues[].
     

    Solution exo 6.3.2 [ Sujet ]

     private void chargerIcones(String repertoire){
      for(int i=0;i<noms_icones_barre.length;i++){
       icones_barre[i]=new ImageIcon(repertoire+noms_icones_barre[i]+".gif");
       icones_barre[3+i] = new ImageIcon(repertoire+noms_icones_barre[i]+"R.gif");
       icones_barre[6+i] = new ImageIcon(repertoire+noms_icones_barre[i]+"B.gif");
      }

      for(int i=0;i<noms_icones_grille.length;i++){
       icones_grille[i]=new ImageIcon(repertoire+noms_icones_grille[i]+".gif");
      }

      for(int i=0;i<noms_icones_dialogues.length;i++){
       icones_dialogues[i]=new ImageIcon(repertoire+noms_icones_dialogues[i]+".gif");
      }
      for(int i=0;i<=8;i++){
       icones_chiffres[i] = new ImageIcon(repertoire+i+".gif");
      }
     }
     

    Constructeur de la classe DemineurTheme

    Maintenant que vous disposez de la méthode qui permet de charger les icônes du thème, vous êtes prêts à écrire le constructeur de la classe :

     DemineurTheme(String p_nom_theme, String repertoire){
      nom_theme=p_nom_theme;
      chargerIcones(repertoire+nom_theme+"/");
    }

    Les argument du constructeur sont les suivants :
    p_nom_theme : représente le nom du thème, et le nom du sous-répertoire qui contient l'ensemble des ressources de ce thème.
    repertoire : représente le nom du répertoire qui contient l'ensemble des sous-répertoires des thèmes.
     

    Méthodes d'accès aux icônes du thème

    Afin de faciliter l'accès aux données de la classe DemineurTheme (il n'est pas facile de retenir l'ensemble des indices qui correspondent à toutes les icônes des différents tableaux), nous allons définir un ensemble de méthodes plus parlantes. Ajoutez les méthodes suivantes dans votre classe :

    Icônes de la grille

    public ImageIcon iconeCool(){
      return icones_grille[3];
     }
    retourne l'icône qui marque une case non dangereuse
    public ImageIcon iconeMine(){
      return icones_grille[1];
     }
    retourne l'icône de la mine
    public ImageIcon iconeStop(){
      return icones_grille[2];
     }
    retourne l'icône qui marque une case dangereuse
     public ImageIcon iconeBase(){
      return icones_grille[0];
     }
    retourne l'icône de base d'une case non explorée
    public ImageIcon iconeChiffre(int i){
      return icones_chiffres[i];
     }
    retourn l'icône qui comporte le chiffre i, pour le marquage des adjacences

     Icônes de la barre d'outils

    public ImageIcon iconeBarreMarche(int type){
      if ((type<0)||(type>2))
      {
       System.out.println("Utilisation incorrecte de la methode ImageIcon iconeBarreMarche");
       System.exit(-1);
      } 
      return  icones_barre[0+type*3];
     }
    retourne l'icône destinée au premier bouton de la barre d'outils (action = marcher sur les cases). L'argument type est une des valeurs  :
  • NSELECT : icône version bouton non sélectionné
  • SELECT  : icône version bouton sélectionné
  • ROLL    : icône pour gestion du rollover
  • public ImageIcon iconeBarreMarqueMine(int type){
      if ((type<0)||(type>2))
      {
       System.out.println("Utilisation incorrecte de la methode ImageIcon iconeBarreMarqueMine");
       System.exit(-1);
      } 
      return  icones_barre[1+type*3];
     }
    retourne l'icône destinée au second bouton de la barre d'outils (action = marquer les cases non dangereuses). L'argument type est une des valeurs  :
  • NSELECT : icône version bouton non sélectionné
  • SELECT  : icône version bouton sélectionné
  • ROLL    : icône pour gestion du rollover
  • public ImageIcon iconeBarreMarqueCool(int type){
      if ((type<0)||(type>2))
      {
       System.out.println("Utilisation incorrecte de la methode ImageIcon iconeBarreMarqueCool");
       System.exit(-1);
      } 
      return  icones_barre[2+type*3];
     }
    etourne l'icône destinée au troisième bouton de la barre d'outils (action = marquer les cases dangereuses). L'argument type est une des valeurs  :
  • NSELECT : icône version bouton non sélectionné
  • SELECT  : icône version bouton sélectionné
  • ROLL    : icône pour gestion du rollover
  • Icônes des dialogues

     public ImageIcon iconeDialogueGagne(){
      return icones_dialogues[0];
     }
    retourne l'icône destinée au dialogue qui s'affiche lorsque le joueur complète le déminage d'une grille.
    public ImageIcon iconeDialoguePerdu(){
      return icones_dialogues[1];
     }
    retourne l'icône destinée au dialogue qui s'affiche lorsque le joueur perd.

    Nom du thème

    public String getNomTheme(){
      return nom_theme;
     }
    retourne simplement le nom du thème

    Contrôle et validation de la classe DemineurTheme

    Exercice 6.3.5. Ecrivez le corps de la méthode suivante :

    JPanel presentationTheme(){
    }

    Elle doit retourner un panneau de la classe JPanel qui comporte l'ensemble des informations présentées ci-dessous (sans la fenêtre, nous la définirons dans la section suivante):

    Le gestionnaire de mise en page du panneau principal est un GridLayout, la grille utilisée est présentée sur l'image précédente. Chaque groupe d'icônes est placé dans un sous-panneau de la classe JPanel, dont le gestionnaire est FlowLayout. Les icônes sont placées sur les panneaux à l'aide d'étiquettes (JLabel). Utilisez les méthodes précédemment définies pour récupérer les icônes voulues, vous ne devez pas accéder directement aux variables privées de la classe Demineur. La méthode presentationTheme méthode servira notamment à tester visuellement le bon fonctionnement de votre classe.

    Aidez-vous du support de TP Swing et/ou de la documentation en ligne pour effectuer cet exercice.

    Solution exo 6.3.5 [ Sujet ] :

     JPanel presentationTheme(){
      JPanel panneau = new JPanel();
      panneau.setLayout(new GridLayout(4,2));
     

      JPanel sous_panneau1 = new JPanel();
      sous_panneau1.setLayout(new FlowLayout());
      sous_panneau1.add(new JLabel(iconeDialoguePerdu()));
      sous_panneau1.add(new JLabel(iconeDialogueGagne()));
      panneau.add(new JLabel("Icones dialogues"));
      panneau.add(sous_panneau1);

      JPanel sous_panneau2 = new JPanel();
      sous_panneau2.setLayout(new FlowLayout());

      for(int i=0;i<3;i++){
       sous_panneau2.add(new JLabel(iconeBarreMarche(i)));
       sous_panneau2.add(new JLabel(iconeBarreMarqueMine(i)));
       sous_panneau2.add(new JLabel(iconeBarreMarqueCool(i)));
      }
      panneau.add(new JLabel("Icones barre d'outils"));
      panneau.add(sous_panneau2);

      JPanel sous_panneau3 = new JPanel();
      sous_panneau3.setLayout(new FlowLayout());
      sous_panneau3.add(new JLabel(iconeBase()));
      sous_panneau3.add(new JLabel(iconeMine()));
      sous_panneau3.add(new JLabel(iconeStop()));
      sous_panneau3.add(new JLabel(iconeCool()));
      panneau.add(new JLabel("Icones de la grille"));
      panneau.add(sous_panneau3);

      JPanel sous_panneau4 = new JPanel();
      sous_panneau4.setLayout(new FlowLayout());
      for(int i=0;i<=8;i++){
       sous_panneau4.add(new JLabel(iconeChiffre(i)));
      }
      panneau.add(new JLabel("Chiffres"));
      panneau.add(sous_panneau4);
      return panneau;
     }
     

    Testons le tout !

    Exercice 6.3.6. Ecrivez la méthode main(String args[]) de votre classe DemineurTheme. Celle-ci se contente de créer une fenêtre simple, le thème "SMILEY" situé dans le répertoire "RESGRAF/", et ajoute le panneau de présentation de ce thème au panneau de contenu de la fenêtre. N'oubliez pas enfin d'indiquer à votre cadre de se dimensionner à la taille des composants qu'il contient, et de s'afficher.
    Vous devriez obtenir un résultat proche de l'image précédente.

    Note : Dans le cas d'un problème avec l'une des icônes (nom de fichier incorrect par exemple), vous ne recevrez aucun message d'erreur, mais l'icône ne s'affichera pas. Pour nous concentrer sur la partie qui nous intéresse , nous laissons la gestion du suivi du chargement des images de côté, le contrôle se fera visuellement.

    Solution exo 6.3.6 [ Sujet ] :

     public static void main(String args[]){
      FenetreSimple cadre = new FenetreSimple("Test theme smiley");
      DemineurTheme theme = new DemineurTheme("SMILEY","RESGRAF/");
      JPanel panneau_theme = theme.presentationTheme();
      cadre.getContentPane().add(panneau_theme);

      cadre.pack();
      cadre.setVisible(true);
     }

    Partie 3 : Un sélecteur de thème

    Etant donné que nous pouvons définir plusieurs thèmes, nous allons écrire une classe qui nous permettra de les visualier. Pour cela nous allons construire un panneau à onglets, chaque fiche comportera un thème différent.

    Créez un nouveau fichier SelecteurTheme.java qui comportera la définition de la classe suivante :

    public class SelecteurTheme extends JTabbedPane{
    }

    Les Variables

    Cette classe comporte une variable privée destinée à stocker les différents thèmes :

    private DemineurTheme table_themes[];

    Le constructeur

    Exercice 6.4.1 Ecirvez le constructeur de cette classe, ce dernier possède la signature suivante :

    SelecteurTheme(String table_noms_themes[], String repertoire)

    les arguments sont les suivants :
    String table_noms_themes[] : une table qui contient la liste de tous les noms de thèmes existant.
    String repertoire : le répertoire qui contient les sous-répertoires des thèmes.

    Le constructeur se charge ensuite :

    Solution exo 6.4.1 [ Sujet ] :

     SelecteurTheme(String table_noms_themes[], String repertoire){
      table_themes = new DemineurTheme[table_noms_themes.length];
      for(int i=0;i<table_themes.length;i++){
       table_themes[i] = new DemineurTheme(table_noms_themes[i], repertoire);
       addTab(table_themes[i].getNomTheme(), table_themes[i].presentationTheme());
      }
     }
     

    La méthode main

    Exercice 6.4.2 Créez la (désormais bien connue) méthode main, qui crée le tableau suivant qui contient la liste des noms des thèmes :

    String table_themes[] = {"SMILEY", "INSECTE", "SKI", "HALLOWEEN"};

    La méthode crée également une fenêtre simple, et ajoute à son panneau de contenu un objet de la classe SelecteurTheme qui devra présenter tous les thèmes dont les noms auront été placés dans le tableau précédent table_themes.

    Vous devriez à présent obtenir une fenêtre contenant quatre onglets permettant de visualiser les icônes de chacun des quatre thèmes :

    Solution exo 6.4.2 [ Sujet ] :

     public static void main(String args[]){
      String table_themes[] = {"SMILEY", "INSECTE", "SKI", "HALLOWEEN"};
      FenetreSimple cadre = new FenetreSimple("Test demineur theme");
      cadre.getContentPane().add(new SelecteurTheme(table_themes, "RESGRAF/"));
      cadre.pack();
      cadre.setVisible(true);
     }

    Partie 4 La barre d'outils

    Nous allons définir la barre d'outils qui permettra de sélectionner le mode d'action durant le jeu. Les modes d'action sont au nombre de trois, et permettent de définir l'action qui sera effectuée lorsque l'utilisateur clique sur une case de la grille. En mode marche le fait de cliquer sur une case permet de révéler ce qu'elle cache : soit le nombre d'adjacences, soit une mine ! Les modes marque_cool et marque_mine permettent de marquer, sans marcher dessus, une case comme étant non dangereuse, ou, plus important, comme étant minée.

    Créez un nouveau fichier nommé ControleAction.java, et qui définit une classe publique de même nom, cette dernière étant une sous-classe de JToolBar qui définit les barres d'outils.

    Constantes

    La classe définit les trois constantes suivantes, qui correspondent aux trois modes d'action précédemment définis :

     public static final int MARCHE = 1;
     public static final int MARQUE_COOL = 2;
     public static final int MARQUE_MINE = 3;

    Variables

    La classe ControleAction comporte tout d'abord une variable theme qui va servir à stocker le thème courant, et une variable action qui définit le mode d'action courant :

      private DemineurTheme theme;
     private int action;

    Les trois variables suivantes concernent les trois boutons radio présents sur la barre : un tableau de boutons radio, un groupe, et une table qui comporte les intitulés des boutons. Les chaines de cette table serviront également dans la gestion des événements associés aux boutons.

     private JRadioButton boutons[] = new JRadioButton[3];
     private ButtonGroup groupe;
     private String noms_boutons[]={"marche", "marque_cool", "marque_mine"};
     

     Méthodes

    Récupération du mode d'action courant

    La première méthode que nous allons ajouter va servir à accéder au mode d'action courant :

     public int getAction(){
      return action;
     }

    C'est l'unique méthode, avec le constructeur que nous définirons plus loin, que nous avons besoin d'atteindre depuis l'extérieur de cette classe.

    Mise en place des composants de la barre d'outils

    Définissons à présent les boutons qui seront placés sur la barre d'outils, en sachant que leur apparence dépendra du thème graphique sélectionné par l'utilisateur. Nous allons tout d'abord écrire deux méthodes utilitaires, pour rendre le code plus clair.

    Exercice 6.5.1 : Ecrivez le corps de la méthode dont la signature est la suivante :

    private JRadioButton initialiserUnBouton(ImageIcon p_ic_nselect, ImageIcon p_ic_select, ImageIcon p_ic_roll, String p_nom);

    Les arguments sont les suivants :
    ImageIcon p_ic_nselect : icône affichée lorsque le bouton n'est pas sélectionné (icône de base) ;
    ImageIcon p_ic_select  : icône affichée lorsque le bouton est sélectionné ;
    ImageIcon p_ic_roll    : icône pour la gestion du rollover ;
    String p_nom           : chaîne qui définit la commande d'action du bouton, elle permettra d'identifier le bouton lors de la gestion des événements.

    La méthode doit créer un nouveau bouton radio, lui affecter de manière appropriée les icônes passées en arguments, activer le rollover, lui associer la commande d'action, et intialiser son état à non sélectionné. Le bouton ainsi créé sera retourné (pas comme une crêpe !) par la méthode.

    Solution exo 6.5.1 [ Sujet ] :

     private JRadioButton initialiserUnBouton(ImageIcon p_ic_nselect, ImageIcon p_ic_select, ImageIcon p_ic_roll, String p_nom){
      JRadioButton bouton = new JRadioButton(p_ic_nselect);
      bouton.setSelectedIcon(p_ic_select);
      bouton.setRolloverIcon(p_ic_roll);
      bouton.setRolloverEnabled(true);
      bouton.setActionCommand(p_nom);
      bouton.setSelected(false);
      return bouton;
     }

    Exercice 6.5.2 : Ecrivez le corps de la méthode suivante :

    private void CreerBoutons(){
    }

    son rôle est simple, elle doit créer les trois boutons de la barre d'outils, les stocker dans la table boutons[] déclarée plus haut, en partant du principe que la variable theme contient le thème courant. Vous utiliserez ici la méthode initialiserUnBouton(...) que nous venons de définir, et les diverses méthodes de la classe DemineurTheme pour obtenir les icônes voulues.

    Les trois boutons font partie d'un groupe de boutons radios, instantiez un tel groupe (qui sera placé dans la variable groupe précédemment définie) et ajoutez les trois boutons à ce groupe. Rappelons que dès lors les boutons sont mutuellement exclusifs, en sélectionner un désélecitonnera les deux autres. C'est tout à fait le comportement qu'il nous faut, puisqu'un seul mode d'action peut être activé à la fois.

    Par défaut le mode d'action doit être MARCHE, et le bouton radio correspondant de la barre doit être sélectionné.

    Solution 6.5.2 [ Sujet ] :

     private void CreerBoutons()
     {
      boutons[0] = initialiserUnBouton(
        theme.iconeBarreMarche(DemineurTheme.NSELECT),
        theme.iconeBarreMarche(DemineurTheme.SELECT),
        theme.iconeBarreMarche(DemineurTheme.ROLL),
        noms_boutons[0]);
      boutons[1] = initialiserUnBouton(
        theme.iconeBarreMarqueCool(DemineurTheme.NSELECT),
        theme.iconeBarreMarqueCool(DemineurTheme.SELECT),
        theme.iconeBarreMarqueCool(DemineurTheme.ROLL),
        noms_boutons[1]);
      boutons[2] = initialiserUnBouton(
        theme.iconeBarreMarqueMine(DemineurTheme.NSELECT),
        theme.iconeBarreMarqueMine(DemineurTheme.SELECT),
        theme.iconeBarreMarqueMine(DemineurTheme.ROLL),
        noms_boutons[2]);
     

      boutons[0].setSelected(true);
      action = MARCHE;

      groupe = new ButtonGroup();
      for(int i=0;i<3;i++){
       groupe.add(boutons[i]);
      }
     }
     

    Construisons !

    Le constructeur de notre classe doit prendre en argument le thème graphique que l'on souhaite utiliser, ce qui nous donne la signature suivante :

    public ControleAction(DemineurTheme p_theme);

    Le rôle du constructeur se contente pour le moment à initialiser la variable theme avec le thème passé en argument, à créer les boutons, et à les ajouter à la barre d'outils que nous sommes en train de construire (n'oubliez pas que la classe ControleAction dérive de JToolBar).

    Exercice 6.5.3 : Ecrivez le constructeur de la classe ControleAction!

    Solution exo 6.5.3 [ Sujet ] :

     public ControleAction(DemineurTheme p_theme){
      theme = p_theme;
      CreerBoutons();
      for(int i=0;i<3;i++){
       add(boutons[i]);
      }
     }
     

    Un petit test intermédiaire

    Exercice 6.5.5 Mettez en place la méthode main(...) de votre classe. Elle doit construire une fenêtre simple, le gestionnaire de mise en page doit être un BorderLayout (rappel : on ne travaille pas directement avec le cadre....mais avec son panneau de contenu ! Créez un thème (SMILEY), et ajoutez dans la partie NORTH du panneau de contenu de votre cadre un nouvel objet de la classe ControleAction. Un pack(), suivi d'un setVisible(true), et vous pouvez compiler et executer votre programme, vous devriez obtenir la belle barre d'outils au design futuriste (notez surtout le soin apporté au graphismes et au choix d'icônes explicites) que voici :

    barre qui ne sert ... strictement à rien si nous ne gérons pas les événements utilisateur.

    Gestion des événements

    Exercice 6.5.5 A vous de jouer ! Créez une classe interne EcouteurRadio qui implémente l'interface ActionListener.
     
    Précisons qu'on dit d'une classe C_A qu'elle est interne à une classe C_B lorsque C_A est définie dans C_B. Ainsi dans notre cas, où EcouteurRadio est interne à ControleAction, nous aurons la structure suivante :
    Extrait du fichier ControleAction.java
    [...]
    public class ControleAction....{

            // méthodes, varialbles, etc. de la classe ControleAction

        class EcouteurRadio.....{

                // méthodes, variables, etc. de la classe interne EcouteurRadio

        }

            // autres méthodes éventuelles de la classe ControleAction
    }

    Les classes internes sont utiles lorsque l'on désire regrouper des classes qui font logiquement partie du même ensemble, tout en contrôlant leur visibilité. Dans notre cas, la classe créée pour la gestion des événements est en quelque sorte locale à la classe ControleAction, elle lui est logiquement associée puisqu' elle gère ses événements d'action, et n'a pas de raison d'être utilisée en dehors de cette classe.


    Implémentez la (seule !) méthode actionPerformed(ActionEvent evt) de cette interface. Son rôle est de traiter les événements en provenance des boutons de la barre d'outils. Ce qui signifie qu'elle va modifier le contenu de la variable action en fonction du bouton qui aura généré l'événement.
    Pour identifier ce bouton, vous utiliserez la commande d'action qui lui a été associée (aidez-vous par exemple du tableau noms_boutons et des méthodes de comparaison de chaînes de la classe String...).

    Solution 6.5.5 [ Sujet ]:

     class EcouteurRadio implements ActionListener{
      public void actionPerformed(ActionEvent evt){
       int i=0;
       while((i<3) && (evt.getActionCommand().compareTo(noms_boutons[i]) != 0))
        i++;

       switch (i){
        case 0: action=MARCHE;
        break;
        case 1 : action = MARQUE_COOL;
        break;
        case 2 : action = MARQUE_MINE;
        break;
       }
      }
     }
     

    Il ne vous reste plus qu'à créer un écouteur d'événements EcouteurRadio dans votre constructeur ControleAction et à l'associer à chacun de vos boutons.

     public ControleAction(DemineurTheme p_theme){
      theme = p_theme;
      EcouteurRadio ecouteur = new EcouteurRadio();
      CreerBoutons();
      for(int i=0;i<3;i++){
       boutons[i].addActionListener(ecouteur);
       add(boutons[i]);
      }
     }
     

    Votre classe ControleAction est fin prête ! Et votre barre d'outils est opérationnelle, il suffira de l'associer à un cadre, et d'employer la méthode getAction() au bon moment pour l'exploiter.

    La correction de la phase II du TP...