Ce cours est visible gratuitement en ligne.

Paperback available in this course

Ce cours existe en eBook.

Certificate of achievement available at the end this course

Got it!
Apprenez à programmer en Java
Last updated on Thursday, June 19, 2014
  • 6 semaines
  • Moyen

Les champs de formulaire

Continuons à explorer les objets que nous propose swing. Ils sont variés et s'utilisent souvent de la même manière que les boutons. En fait, maintenant que nous avons compris le fonctionnement du pattern observer, nous travaillerons avec des interfaces et devrons donc implémenter des méthodes pour gérer les événements avec nos composants.
Allons-y !

Les listes : l'objet JComboBox

Première utilisation

Comme à l'accoutumée, nous utiliserons d'abord cet objet dans un contexte exempt de tout code superflu. Créons donc un projet avec une classe contenant la méthode main() et une classe héritée de JFrame.

Dans cet exemple, nous aurons bien sûr besoin d'une liste, faites-en une. Cependant, vous ne manquerez pas de constater que notre objet est ridiculement petit. Vous connaissez le remède : il suffit de lui spécifier une taille !

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame {
  private JPanel container = new JPanel();
  private JComboBox combo = new JComboBox();
  private JLabel label = new JLabel("Une ComboBox");

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    combo.setPreferredSize(new Dimension(100, 20));

    JPanel top = new JPanel();
    top.add(label);
    top.add(combo);
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);            
  }
}

En revanche, cette liste est vide ! Pour résoudre ce problème, il suffit d'utiliser la méthode addItem(Object obj).

Voici le nouveau code :

//Les imports restent inchangés
 
public class Fenetre extends JFrame {
  //Les variables d'instance restent inchangées

  public Fenetre(){
    //…
    combo.setPreferredSize(new Dimension(100, 20));
    combo.addItem("Option 1");
    combo.addItem("Option 2");
    combo.addItem("Option 3");
    combo.addItem("Option 4");

    //…         
  }
}

Vous pouvez voir ce que ça donne à la figure suivante.

JComboBox contenant des données
JComboBox contenant des données

Pour initialiser une JComboBox, vous pouvez utiliser le constructeur prenant un tableau d'objets en paramètre afin de renseigner tous les éléments d'un coup. Ceci est donc équivalent au code précédent :

String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"};
combo = new JComboBox(tab);

Vous pouvez assigner un choix par défaut avec la méthode setSelectedIndex(int index). Vous avez aussi la possibilité de changer la couleur du texte, la couleur de fond ou la police, exactement comme avec un JLabel.

Depuis Java 7, l'objet JComboBox peut être paramétré avec un type générique, comme ceci : JComboBox<String> combo = new JComboBox<String>(); ce qui permet de mieux gérer le contenu de nos listes et ainsi mieux récupérer les valeurs de ces dernières.

Maintenant que nous savons comment fonctionne cet objet, nous allons apprendre à communiquer avec lui.

L'interface ItemListener

Cette interface possède une méthode à redéfinir. Celle-ci est appelée lorsqu'un élément a changé d'état. Puisqu'un exemple est toujours plus éloquent, voici un code implémentant cette interface :

//Les autres imports
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
 
public class Fenetre extends JFrame {
  //Les variables d'instance restent inchangées

  public Fenetre(){
    //Le début ne change pas

    //Ici, nous changeons juste la façon d'initialiser la JComboBox
    String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"};
    combo = new JComboBox(tab);

    //Ajout du listener
    combo.addItemListener(new ItemState());
    combo.setPreferredSize(new Dimension(100, 20));
    combo.setForeground(Color.blue);

    //La fin reste inchangée      
  }

  //Classe interne implémentant l'interface ItemListener
  class ItemState implements ItemListener{
    public void itemStateChanged(ItemEvent e) {
      System.out.println("événement déclenché sur : " + e.getItem());
    }               
  }
}

Dans mon exemple, j'ai cliqué sur Option 2, puis Option 3, puis Option 4, ce qui correspond à la figure suivante.

Interaction avec la JComboBox
Interaction avec la JComboBox

Vous voyez que lorsque nous cliquons sur une autre option, notre objet commence par modifier l'état de l'option précédente (l'état passe en DESELECTED) avant de changer celui de l'option choisie (celle-ci passe à l'état SELECTED). Nous pouvons donc suivre très facilement l'état de nos éléments grâce à cette interface ; cependant, pour plus de simplicité, nous utiliserons l'interface ActionListener afin de récupérer l'option sélectionnée.

Voici un code implémentant cette interface :

//Les autres imports
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Fenetre extends JFrame {
  //Les variables d'instance restent inchangées        
  public Fenetre(){
    //Le début ne change pas
    String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"};
    combo = new JComboBox(tab);
    //Ajout du listener
    combo.addItemListener(new ItemState());
    combo.addActionListener(new ItemAction());
    combo.setPreferredSize(new Dimension(100, 20));
    combo.setForeground(Color.blue);
    //La fin reste inchangée       
  }

  //La classe interne ItemState reste inchangée

  class ItemAction implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      System.out.println("ActionListener : action sur " + combo.getSelectedItem());
    }               
  }
}

Le résultat se trouve à la figure suivante.

ActionListener et JComboBox
ActionListener et JComboBox

Vous constatez qu'en utilisant cette méthode, nous pouvons récupérer l'option sur laquelle l'action a été effectuée. L'appel de la méthode getSelectedItem() retourne la valeur de l'option sélectionnée ; une fois récupérée, nous pouvons travailler avec notre liste !

Maintenant que nous savons comment récupérer les informations dans une liste, je vous invite à continuer notre animation.

Changer la forme de notre animation

Comme le titre l'indique, nous allons faire en sorte que notre animation ne se contente plus d'afficher un rond : nous pourrons désormais choisir la forme que nous voulons afficher. Bien sûr, je ne vais pas vous faire réaliser toutes les formes possibles et imaginables ; je vous en fournis quelques-unes et, si le cœur vous en dit, vous pouvez ajouter des formes de votre composition.

Très bien : pour réaliser cela, nous devons dynamiser un peu notre classe Panneau afin qu'elle peigne une forme en fonction de notre choix.
Pour y parvenir, nous allons ajouter une variable d'instance de type String qui contiendra l'intitulé de la forme que nous souhaitons dessiner - appelons-la forme - ainsi qu'un mutateur permettant de redéfinir cette variable.

Notre méthode paintComponent() doit pouvoir dessiner la forme demandée ; ainsi, trois cas de figure se profilent :

  • soit nous intégrons les instructions if dans cette méthode et l'objet Graphics dessinera en fonction de la variable ;

  • soit nous développons une méthode privée appelée dans la méthode paintComponent() et qui dessinera la forme demandée ;

  • soit nous utilisons le pattern strategy afin d'encapsuler la façon dont nous dessinerons nos formes dans notre animation.

Le pattern strategy est de loin la meilleure solution, mais afin de ne pas alourdir nos exemples, nous travaillerons « à l'ancienne ».

Nous allons donc développer une méthode privée - appelons-la draw(Graphics g) - qui aura pour tâche de dessiner la forme voulue. Nous passerons l'objet Graphics dans la méthode paintComponent() de sorte que cette dernière puisse l'utiliser ; c'est donc dans cette méthode que nous placerons nos conditions.

Je vous propose les formes suivantes :

  • le rond, forme par défaut ;

  • le carré ;

  • le triangle ;

  • l'étoile (soyons fous).

Cela signifie que notre liste contiendra ces quatre choix et que le rond figurera en premier lieu. Nous créerons aussi une implémentation d'ActionListener dans une classe interne pour gérer les actions de notre liste. Je l'ai appelée FormeListener (c'est fou ce que je suis original).

Ce que vous obtiendrez est représenté à la figure suivante.

Différents rendus de l'application
Différents rendus de l'application

Essayez de réaliser ces formes vous-mêmes : il n'y a là rien de compliqué, je vous assure ! Bon, l'étoile est peut-être un peu plus complexe que les autres, mais ce n'est pas insurmontable.

Classe Panneau
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
 
import javax.swing.JPanel;
 
public class Panneau extends JPanel {
  private int posX = -50;
  private int posY = -50;
  private String forme = "ROND";

  public void paintComponent(Graphics g){
    //On choisit une couleur de fond pour le rectangle
    g.setColor(Color.white);
    //On le dessine de sorte qu'il occupe toute la surface
    g.fillRect(0, 0, this.getWidth(), this.getHeight());
    //On redéfinit une couleur pour le rond
    g.setColor(Color.red);
    //On délègue la méthode de dessin à la méthode draw()
    draw(g);      
  }
 
  private void draw(Graphics g){      
    if(this.forme.equals("ROND")){
      g.fillOval(posX, posY, 50, 50); 
    }
    if(this.forme.equals("CARRE")){
      g.fillRect(posX, posY, 50, 50);
    }
    if(this.forme.equals("TRIANGLE")){
      //Calcul des sommets            
      //Le sommet 1 se situe à la moitié du côté supérieur du carré
      int s1X = posX + 25;
      int s1Y = posY;
      //Le sommet 2 se situe en bas à droite
      int s2X = posX + 50;
      int s2Y = posY + 50;
      //Le sommet 3 se situe en bas à gauche
      int s3X = posX;
      int s3Y = posY + 50;      
      //Nous créons deux tableaux de coordonnées
      int[] ptsX = {s1X, s2X, s3X};
      int[] ptsY = {s1Y, s2Y, s3Y};      
      //Nous utilisons la méthode fillPolygon()
      g.fillPolygon(ptsX, ptsY, 3);
    }
    if(this.forme.equals("ETOILE")){      
      //Pour l'étoile, on se contente de tracer des lignes dans le carré
      //correspondant à peu près à une étoile...
      //Mais ce code peut être amélioré !
      int s1X = posX + 25;
      int s1Y = posY;
      int s2X = posX + 50;
      int s2Y = posY + 50;        
      g.drawLine(s1X, s1Y, s2X, s2Y);      
      int s3X = posX;
      int s3Y = posY + 17;
      g.drawLine(s2X, s2Y, s3X, s3Y);      
      int s4X = posX + 50;
      int s4Y = posY + 17;
      g.drawLine(s3X, s3Y, s4X, s4Y);            
      int s5X = posX;
      int s5Y = posY + 50;
      g.drawLine(s4X, s4Y, s5X, s5Y);       
      g.drawLine(s5X, s5Y, s1X, s1Y);  
    }
  }
  
  public void setForme(String form){
    this.forme = form;
  }
  
  public int getPosX() {
    return posX;
  }
 
  public void setPosX(int posX) {
    this.posX = posX;
  }
 
  public int getPosY() {
    return posY;
  }
 
  public void setPosY(int posY) {
    this.posY = posY;
  }
}
Classe Fenetre
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
 
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame{
  private Panneau pan = new Panneau();
  private JButton bouton = new JButton("Go");
  private JButton bouton2 = new JButton("Stop");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Choix de la forme");
  private int compteur = 0;
  private boolean animated = true;
  private boolean backX, backY;
  private int x, y;
  private Thread t;  
  private JComboBox combo = new JComboBox();
  
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
 
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);
    
    bouton.addActionListener(new BoutonListener());     
    bouton2.addActionListener(new Bouton2Listener());
    bouton2.setEnabled(false);    
    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);
    
    combo.addItem("ROND");
    combo.addItem("CARRE");
    combo.addItem("TRIANGLE");
    combo.addItem("ETOILE");    
    combo.addActionListener(new FormeListener());
    
    JPanel top = new JPanel();
    top.add(label);
    top.add(combo);    
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);          
  }

  private void go(){
    x = pan.getPosX();
    y = pan.getPosY();
    while(this.animated){
      if(x < 1) backX = false;
      if(x > pan.getWidth() - 50) backX = true;    
      if(y < 1) backY = false;
      if(y > pan.getHeight() - 50) backY = true;      
      if(!backX) pan.setPosX(++x);
      else pan.setPosX(--x);
      if(!backY) pan.setPosY(++y);
      else pan.setPosY(--y);
      pan.repaint();
      try {
        Thread.sleep(3);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }    
  }
 
  //Classe écoutant notre bouton
  public class BoutonListener implements ActionListener{
     public void actionPerformed(ActionEvent arg0) {
      animated = true;
      t = new Thread(new PlayAnimation());
      t.start();
      bouton.setEnabled(false);
      bouton2.setEnabled(true); 
    }
  }
  
  class Bouton2Listener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      animated = false;  
      bouton.setEnabled(true);
      bouton2.setEnabled(false);
    }  
  }  
  
  class PlayAnimation implements Runnable{
    public void run() {
      go();    
    }  
  }
    
  class FormeListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      //La méthode retourne un Object puisque nous passons des Object dans une liste
      //Il faut donc utiliser la méthode toString() pour retourner un String (ou utiliser un cast)
      pan.setForme(combo.getSelectedItem().toString());
    }  
  }    
}

Et voilà le travail ! Vous avez vu : il n'y avait rien de sorcier. En fait, étant donné que vous avez l'habitude d'utiliser des objets graphiques et des implémentations d'interfaces, les choses vont maintenant s'accélérer, car le principe est le même pour tous les objets graphiques de base.

Les cases à cocher : l'objet JCheckBox

Première utilisation

Créez un projet vide avec une classe contenant une méthode main() et une classe héritant de JFrame. Cela fait, nous allons utiliser notre nouvel objet. Celui-ci peut être instancié avec un String en paramètre qui servira de libellé.

Nous pouvons également cocher la case par défaut en appelant la méthode setSelected(Boolean bool) à laquelle nous passons true. Cet objet possède, comme tous les autres, une multitude de méthodes nous simplifiant la vie ; je vous invite aussi à fouiner un peu…

Nous créerons directement une implémentation de l'interface ActionListener, vous connaissez bien la démarche. Contrôlons également que notre objet est coché à l'aide de la méthode isSelected() qui retourne un booléen. Voici un code mettant tout cela en œuvre :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame {
  private JPanel container = new JPanel();
  private JCheckBox check1 = new JCheckBox("Case 1");
  private JCheckBox check2 = new JCheckBox("Case 2");
      
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    JPanel top = new JPanel();
    check1.addActionListener(new StateListener());
    check2.addActionListener(new StateListener());
    top.add(check1);
    top.add(check2);
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);        
  }     
      
  class StateListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      System.out.println("source : " + ((JCheckBox)e.getSource()).getText() + " - état : " + ((JCheckBox)e.getSource()).isSelected());
    }
  }
}

Le résultat se trouve à la figure suivante.

Nos cases à cocher
Nos cases à cocher

Ici, je me suis amusé à cocher et décocher mes cases. Il n'y a rien de bien difficile, ça devient routinier, non ?

Un pseudomorphing pour notre animation

Nous allons utiliser cet objet afin que nos formes changent de taille et proposent un pseudo-effet de morphing.

Premièrement, la taille de notre forme est fixe, il nous faut changer cela. Allez, hop, une variable de type int dans notre classe Panneau - disons drawSize - initialisée à 50. Tout comme avec le déplacement, nous devons savoir lorsqu'il faut augmenter ou réduire la taille de notre forme : nous utiliserons donc la même méthode que celle que nous avions développée à ce moment-là.

Un JCheckBox sera nécessaire pour savoir si le « mode morphing » est activé.

En ce qui concerne la taille, si on la réduit ou l'augmente d'une unité à chaque rafraîchissement, l'effet de morphing sera ultra rapide. Donc, pour ralentir l'effet, nous utiliserons une méthode retournant 1 ou 0 selon le nombre de rafraîchissements. Cela implique que nous aurons besoin d'une variable pour les dénombrer. Nous effectuerons une augmentation ou une réduction toutes les dix fois.

Pour bien séparer les deux cas de figure, nous insérerons une deuxième méthode de dessin dans la classe Panneau qui aura pour rôle de dessiner le morphing ; appelons-la drawMorph(Graphics g).

Lorsque nous cocherons la case, le morphing s'activera, et il se désactivera une fois décochée. La classe Panneau devra donc disposer d'un mutateur pour le booléen de morphing.

Souvenez-vous que nous gérons la collision avec les bords dans notre classe Fenetre. Cependant, en « mode morphing », la taille de notre forme n'est plus constante : il faudra gérer ce nouveau cas de figure dans notre méthode go(). Notre classe Panneau devra posséder un accesseur permettant de retourner la taille actuelle de la forme.

Vous avez désormais toutes les clés en main pour réussir cette animation.

La figure suivante donne un aperçu de ce que vous devriez obtenir (je n'ai représenté que le rond et le triangle, mais ça fonctionne avec toutes les formes).

Morphing
Morphing
Fichier Panneau.java
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D; 
import javax.swing.JPanel;
 
public class Panneau extends JPanel { 
  private int posX = -50;
  private int posY = -50;
  private int drawSize = 50;
  //Un booléen pour le mode morphing 
  //Un autre pour savoir si la taille doit être réduite
  private boolean morph = false, reduce = false;
  private String forme = "ROND";
  //Le compteur de rafraîchissements
  private int increment = 0;
  
  public void paintComponent(Graphics g){
    g.setColor(Color.white);
    g.fillRect(0, 0, this.getWidth(), this.getHeight());
    g.setColor(Color.red);
    //Si le mode morphing est activé, on peint le morphing
    if(this.morph)
      drawMorph(g);
    //Sinon, on peint le mode normal
    else
      draw(g);    
  }
 
  private void draw(Graphics g){
    if(this.forme.equals("ROND")){
      g.fillOval(posX, posY, 50, 50); 
    }
    if(this.forme.equals("CARRE")){
      g.fillRect(posX, posY, 50, 50);
    }
    if(this.forme.equals("TRIANGLE")){       
      int s1X = posX + 50/2;
      int s1Y = posY;
      int s2X = posX + 50;
      int s2Y = posY + 50;
      int s3X = posX;
      int s3Y = posY + 50;      
      int[] ptsX = {s1X, s2X, s3X};
      int[] ptsY = {s1Y, s2Y, s3Y};      
      g.fillPolygon(ptsX, ptsY, 3);
    }
    if(this.forme.equals("ETOILE")){      
      int s1X = posX + 50/2;
      int s1Y = posY;
      int s2X = posX + 50;
      int s2Y = posY + 50;        
      g.drawLine(s1X, s1Y, s2X, s2Y);      
      int s3X = posX;
      int s3Y = posY + 50/3;
      g.drawLine(s2X, s2Y, s3X, s3Y);      
      int s4X = posX + 50;
      int s4Y = posY + 50/3;
      g.drawLine(s3X, s3Y, s4X, s4Y);                   
      int s5X = posX;
      int s5Y = posY + 50;
      g.drawLine(s4X, s4Y, s5X, s5Y);       
      g.drawLine(s5X, s5Y, s1X, s1Y);  
    }    
  }
  
  //Méthode qui peint le morphing
  private void drawMorph(Graphics g){
    //On incrémente
    increment++;
    //On regarde si on doit réduire ou non
    if(drawSize >= 50) reduce = true;
    if(drawSize <= 10) reduce = false;    
    if(reduce)
      drawSize = drawSize - getUsedSize();
    else
      drawSize = drawSize + getUsedSize();
    
    if(this.forme.equals("ROND")){
      g.fillOval(posX, posY, drawSize, drawSize); 
    }
    if(this.forme.equals("CARRE")){
      g.fillRect(posX, posY, drawSize, drawSize);
    }
    if(this.forme.equals("TRIANGLE")){        
      int s1X = posX + drawSize/2;
      int s1Y = posY;
      int s2X = posX + drawSize;
      int s2Y = posY + drawSize;
      int s3X = posX;
      int s3Y = posY + drawSize;      
      int[] ptsX = {s1X, s2X, s3X};
      int[] ptsY = {s1Y, s2Y, s3Y};      
      g.fillPolygon(ptsX, ptsY, 3);
    }
    if(this.forme.equals("ETOILE")){      
      int s1X = posX + drawSize/2;
      int s1Y = posY;
      int s2X = posX + drawSize;
      int s2Y = posY + drawSize;      
      g.drawLine(s1X, s1Y, s2X, s2Y);      
      int s3X = posX;
      int s3Y = posY + drawSize/3;
      g.drawLine(s2X, s2Y, s3X, s3Y);      
      int s4X = posX + drawSize;
      int s4Y = posY + drawSize/3;
      g.drawLine(s3X, s3Y, s4X, s4Y);                   
      int s5X = posX;
      int s5Y = posY + drawSize;
      g.drawLine(s4X, s4Y, s5X, s5Y);       
      g.drawLine(s5X, s5Y, s1X, s1Y);  
    }    
  }
  
  //Retourne le nombre à retrancher ou à ajouter pour le morphing
  private int getUsedSize(){
    int res = 0;
    //Si le nombre de tours est de dix, on réinitialise l'incrément et on retourne 1
    if(increment / 10 == 1){
      increment = 0;
      res = 1;
    }    
    return res;
  }
  
  public int getDrawSize(){
    return drawSize;
  }
  
  public boolean isMorph(){
    return morph;
  }
  
  public void setMorph(boolean bool){
    this.morph = bool;
    //On réinitialise la taille
    drawSize = 50;
  }
  
  public void setForme(String form){
    this.forme = form;
  }
  
  public int getPosX() {
    return posX;
  }
 
  public void setPosX(int posX) {
    this.posX = posX;
  }
 
  public int getPosY() {
    return posY;
  }
 
  public void setPosY(int posY) {
    this.posY = posY;
  }
}
Fichier Fenetre.java
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame{

  private Panneau pan = new Panneau();
  private JButton bouton = new JButton("Go");
  private JButton bouton2 = new JButton("Stop");
  private JPanel container = new JPanel();
  private JLabel label = new JLabel("Choix de la forme");
  private int compteur = 0;
  private boolean animated = true;
  private boolean backX, backY;
  private int x, y;
  private Thread t;
  private JComboBox combo = new JComboBox();
  
  private JCheckBox morph = new JCheckBox("Morphing");
  
  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null); 
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    container.add(pan, BorderLayout.CENTER);    
    bouton.addActionListener(new BoutonListener());     
    bouton2.addActionListener(new Bouton2Listener());
    bouton2.setEnabled(false);    
    JPanel south = new JPanel();
    south.add(bouton);
    south.add(bouton2);
    container.add(south, BorderLayout.SOUTH);    
    combo.addItem("ROND");
    combo.addItem("CARRE");
    combo.addItem("TRIANGLE");
    combo.addItem("ETOILE");    
    combo.addActionListener(new FormeListener());    
    morph.addActionListener(new MorphListener());
     
    JPanel top = new JPanel();
    top.add(label);
    top.add(combo);
    top.add(morph);    
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);         
  }
      
  private void go(){
    x = pan.getPosX();
    y = pan.getPosY();
    while(this.animated){
    
    //Si le mode morphing est activé, on utilise la taille actuelle de la forme
      if(pan.isMorph()){
        if(x < 1)backX = false;
        if(x > pan.getWidth() - pan.getDrawSize()) backX = true;   
        if(y < 1)backY = false;
        if(y > pan.getHeight() - pan.getDrawSize()) backY = true;
      }
    //Sinon, on fait comme d'habitude
      else{
        if(x < 1)backX = false;
        if(x > pan.getWidth()-50) backX = true;    
        if(y < 1)backY = false;
        if(y > pan.getHeight()-50) backY = true;
      }  

      if(!backX) pan.setPosX(++x);
      else pan.setPosX(--x);
      if(!backY) pan.setPosY(++y);
      else pan.setPosY(--y);
      pan.repaint();
      try {
        Thread.sleep(3);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }    
  }

  public class BoutonListener implements ActionListener{
    public void actionPerformed(ActionEvent arg0) {
      animated = true;
      t = new Thread(new PlayAnimation());
      t.start();
      bouton.setEnabled(false);
      bouton2.setEnabled(true);       
    }    
  }
    
  class Bouton2Listener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      animated = false;    
      bouton.setEnabled(true);
      bouton2.setEnabled(false);
    }    
  }    
    
  class PlayAnimation implements Runnable{
    public void run() {
      go();      
    }    
  }
    
  class FormeListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      pan.setForme(combo.getSelectedItem().toString());
    }    
  }
    
  class MorphListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      //Si la case est cochée, on active le mode morphing
      if(morph.isSelected())pan.setMorph(true);
      //Sinon, on ne fait rien
      else pan.setMorph(false);
    }
  }    
}

Alors, qu'en pensez-vous ? J'aime bien, moi… Vous voyez, l'utilisation des JCheckBox est très simple. Je vous propose maintenant d'étudier un de ses cousins !

Le petit cousin : l'objet JRadioButton

Le voici, le cousin éloigné… Le principe est de proposer au moins deux choix, mais de ne permettre d'en sélectionner qu'un à la fois. L'instanciation se fait de la même manière que pour un JCheckBox ; d'ailleurs, nous utiliserons l'exemple du début de ce chapitre en remplaçant les cases à cocher par des boutons radio. Voici le code correspondant :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
  
public class Fenetre extends JFrame {
  private JPanel container = new JPanel();
  private JRadioButton jr1 = new JRadioButton("Radio 1");
  private JRadioButton jr2 = new JRadioButton("Radio 2");

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    JPanel top = new JPanel();
    jr1.addActionListener(new StateListener());
    jr2.addActionListener(new StateListener());
    top.add(jr1);
    top.add(jr2);
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);            
  }       
        
  class StateListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      System.out.println("source : " + ((JRadioButton)e.getSource()).getText() + " - état : " + ((JRadioButton)e.getSource()).isSelected());
    }
  }
}

Le résultat est représenté à la figure suivante.

Test avec un groupe de boutons
Test avec un groupe de boutons

Vous pouvez voir que cet objet s'utilise de la même manière que le précédent. Le problème, ici, c'est que nous pouvons sélectionner les deux options (alors que ce n'est normalement pas possible). Pour qu'un seul bouton radio soit sélectionné à la fois, nous devons définir un groupe de boutons à l'aide de ButtonGroup. Nous y ajouterons nos boutons radio, et seule une option pourra alors être sélectionnée.

//Les autres imports 
import javax.swing.ButtonGroup; 
 
public class Fenetre extends JFrame {
  //Les autres variables
  private ButtonGroup bg = new ButtonGroup();

  public Fenetre(){
    //Les autres instructions       
    jr1.setSelected(true);
    jr1.addActionListener(new StateListener());
    jr2.addActionListener(new StateListener());
    //On ajoute les boutons au groupe
    bg.add(jr1);
    bg.add(jr2);
    top.add(jr1);
    top.add(jr2);
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);            
  }       
        
  class StateListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      System.out.println("source : " + jr1.getText() + " - état : " + jr1.isSelected());
      System.out.println("source : " + jr2.getText() + " - état : " + jr2.isSelected());
    }
  }
}

Voyez le résultat à la figure suivante.

Test des boutons radio
Test des boutons radio

Les champs de texte : l'objet JTextField

Première utilisation

Je pense que vous savez ce que vous avez à faire. Si ce n'est pas déjà fait, créez un nouveau projet contenant les classes habituelles. Comme l'indique le titre de cette partie, nous allons utiliser l'objet JTextField. Vous vous en doutez, cet objet propose lui aussi des méthodes de redimensionnement, de changement de couleur… De ce fait, je commence avec un exemple complet. Lisez et testez ce code :

//Les imports habituels
import javax.swing.JTextField;
 
public class Fenetre extends JFrame {
  private JPanel container = new JPanel();
  private JTextField jtf = new JTextField("Valeur par défaut");
  private JLabel label = new JLabel("Un JTextField");

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    JPanel top = new JPanel();
    Font police = new Font("Arial", Font.BOLD, 14);
    jtf.setFont(police);
    jtf.setPreferredSize(new Dimension(150, 30));
    jtf.setForeground(Color.BLUE);
    top.add(label);
    top.add(jtf);
    container.add(top, BorderLayout.NORTH);
    this.setContentPane(container);
    this.setVisible(true);            
  }
}

Cela donne la figure suivante.

Exemple de champ de texte
Exemple de champ de texte

Nous pouvons initialiser le contenu avec la méthode setText(String str) ou le récupérer grâce à la méthode getText().

Il existe un objet très ressemblant à celui-ci, en un peu plus étoffé. En fait, cet objet permet de créer un JTextField formaté pour recevoir un certain type de données saisies (date, pourcentage etc.). Voyons cela tout de suite.

Un objet plus restrictif : le JFormattedTextField

Grâce à ce type d'objet, nous pourrons éviter beaucoup de contrôles et de casts sur le contenu de nos zones de texte. Si vous avez essayé de récupérer le contenu du JTextField utilisé ci-dessus (lors du clic sur un bouton, par exemple), vous avez dû vous rendre compte que le texte qu'il contenait importait peu, mais un jour, vous aurez sans doute besoin d'une zone de texte qui n'accepte qu'un certain type de données. Avec l'objet JFormattedTextField, nous nous en approchons (mais vous verrez que vous pourrez faire encore mieux). Cet objet retourne une valeur uniquement si celle-ci correspond à ce que vous avez autorisé. Je m'explique : si vous voulez que votre zone de texte contienne par exemple des entiers et rien d'autre, c'est possible ! En revanche, ce contrôle ne s'effectue que lorsque vous quittez le champ en question. Vous pouvez ainsi saisir des lettres dans un objet n'acceptant que des entiers, mais la méthode getText() ne renverra alors rien, car le contenu sera effacé, les données ne correspondent pas aux attentes de l'objet. Voici un code et deux exemples, ainsi que leur rendu (figure suivante).

//Les imports habituels
 
public class Fenetre extends JFrame {
  private JPanel container = new JPanel();
  private JFormattedTextField jtf = new JFormattedTextField(NumberFormat.getIntegerInstance());
  private JFormattedTextField jtf2 = new JFormattedTextField(NumberFormat.getPercentInstance());
  private JLabel label = new JLabel("Un JTextField");
  private JButton b = new JButton ("OK");

  public Fenetre(){
    this.setTitle("Animation");
    this.setSize(300, 300);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);
    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());
    JPanel top = new JPanel();        
    Font police = new Font("Arial", Font.BOLD, 14);
    jtf.setFont(police);
    jtf.setPreferredSize(new Dimension(150, 30));
    jtf.setForeground(Color.BLUE);
    jtf2.setPreferredSize(new Dimension(150, 30));
    b.addActionListener(new BoutonListener());
    top.add(label);
    top.add(jtf);
    top.add(jtf2);
    top.add(b);
    this.setContentPane(top);
    this.setVisible(true);            
  }       

  class BoutonListener implements ActionListener{
    public void actionPerformed(ActionEvent e) {
      System.out.println("TEXT : jtf " + jtf.getText());
      System.out.println("TEXT : jtf2 " + jtf2.getText());
    }
  }
}
Exemple valide à gauche et invalide à droite
Exemple valide à gauche et invalide à droite

Vous voyez qu'en plus, notre objet met automatiquement la saisie en forme lorsqu'elle est valide : il espace les nombres tous les trois chiffres afin d'en faciliter la lecture.

Voici ce que vous pouvez utiliser dans ce genre de champ :

  • NumberFormat avec :

    • getIntegerInstance()

    • getPercentInstance()

    • getNumberInstance()

  • DateFormat avec

    • getTimeInstance()

    • getDateInstance()

  • MessageFormat

Sans entrer dans les détails, vous pouvez aussi utiliser un objet MaskFormatter qui permet d'attribuer un format de longueur fixe à votre zone de texte. C'est très pratique lorsque vous souhaitez introduire un numéro de téléphone, un numéro de sécurité sociale etc.

Vous devez définir ce format avec un paramètre lors de l'instanciation du masque à l'aide de métacaractères. Ceux-ci indiquent à votre objet MaskFormatter ce que le contenu de votre zone de texte contiendra. Voici la liste de ces métacaractères :

  • # : indique un chiffre ;

  • ' : indique un caractère d'échappement ;

  • U : indique une lettre (les minuscules sont automatiquement changées en majuscules) ;

  • L : indique une lettre (les majuscules sont automatiquement changées en minuscules) ;

  • A : indique un chiffre ou une lettre ;

  • ? : indique une lettre ;

  • * : indique que tous les caractères sont acceptés ;

  • H : indique que tous les caractères hexadécimaux sont acceptés (0 à 9, a à f et A à F).

Voici à quoi ressemblerait un format téléphonique :

try{
  MaskFormatter tel = new MaskFormatter("## ## ## ## ##");
  //Ou encore
  MaskFormatter tel2 = new MaskFormatter("##-##-##-##-##");
  //Vous pouvez ensuite le passer à votre zone de texte
  JFormattedTextField jtf = new JFormattedTextField(tel2);
}catch(ParseException e){e.printStackTrace();}

Vous voyez qu'il n'y a là rien de compliqué. Je vous invite à essayer cela dans le code précédent, vous constaterez qu'avec le métacaractère utilisé dans notre objet MaskFormatter, nous sommes obligés de saisir des chiffres. La figure suivante montre le résultat après avoir cliqué sur le bouton.

Essai avec un MaskFormatter
Essai avec un MaskFormatter

Je ne sais pas pour le numéro de téléphone américain, mais le numéro français est loin d'être un numéro de téléphone valide. Nous voici confrontés à un problème qui nous hantera tant que nous programmerons : l'intégrité de nos données !

Comme le montre l'exemple précédent, nous pouvons suggérer à l'utilisateur ce qu'il doit renseigner comme données dans les champs, mais nous ne devons pas lui faire aveuglément confiance ! C'est simple : on part du principe de ne jamais faire confiance à l'utilisateur.

Nous sommes donc obligés d'effectuer une multitude de contrôles supplémentaires. Pour ce faire, nous pouvons :

  • tester chaque élément du numéro ;

  • tester le numéro en entier ;

  • dans le cas où nous n'utilisons pas de MaskFormatter, vérifier en plus que les saisies sont numériques ;

  • utiliser une expression régulière ;

  • empêcher la saisie d'un type de caractères ;

  • etc.

En gros, nous devons vérifier l'intégrité de nos données (dans le cas qui nous intéresse, l'intégrité de nos chaînes de caractères) pendant ou après la saisie. Je ne vous cache pas que cela prendra une grande part de votre temps lorsque vous coderez vos propres logiciels, mais c'est le métier qui veut ça.

Avant de terminer ce chapitre (assez conséquent, je l'avoue), je vous propose de voir comment nous pouvons récupérer les événements du clavier. Nous avons appris à interagir avec la souris, mais pas avec le clavier.

Contrôle du clavier : l'interface KeyListener

Nous connaissons déjà :

  • l'interface MouseListener qui interagit avec la souris ;

  • l'interface ActionListener qui interagit lors d'un clic sur un composant ;

  • l'interface ItemListener qui écoute les événements sur une liste déroulante.

Voici à présent l'interface KeyListener. Comme l'indique le titre, elle nous permet d'intercepter les événements clavier lorsque l'on :

  • presse une touche ;

  • relâche une touche ;

  • tape sur une touche.

Vous savez ce qu'il vous reste à faire : créer une implémentation de cette interface dans votre projet. Créez une classe interne qui l'implémente et utilisez l'astuce d'Eclipse pour générer les méthodes nécessaires.

Vous constatez qu'il y en a trois :

  • keyPressed(KeyEventevent), appelée lorsqu'on presse une touche ;

  • keyReleased(KeyEventevent), appelée lorsqu'on relâche une touche (c'est à ce moment que le composant se voit affecter la valeur de la touche) ;

  • keyTyped(KeyEventevent), appelée entre les deux méthodes citées ci-dessus.

Comme vous vous en doutez, l'objet KeyEvent nous permettra d'obtenir des informations sur les touches qui ont été utilisées. Parmi celles-ci, nous utiliserons :

  • getKeyCode() : retourne le code de la touche ;

  • getKeyChar() : retourne le caractère correspondant à la touche.

Nous pouvons aussi déterminer lorsque certaines touches de contrôle ont été utilisées (SHIFT, CTRL…), connaître le composant à l'origine de l'événement, etc. Nous n'en parlerons pas ici, mais ce genre d'information est facile à trouver sur Internet.

Pour des raisons de simplicité, nous n'utiliserons pas un JFormattedTextField mais un JTextField sans MaskFormatter. Ainsi, nous n'aurons pas à nous préoccuper des tirets de notre champ.

Pour commencer, nous allons examiner l'ordre dans lequel se déroulent les événements clavier ; il est vrai que ceux-ci se produisent si rapidement que nous n'avons pas le temps de les voir défiler. J'ai donc ajouté une pause à la fin de chaque méthode de l'implémentation afin de mieux observer l'ordre d'exécution.

Voici le code source que nous allons utiliser (il est presque identique aux précédents, rassurez-vous) :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class Fenetre extends JFrame {

  private JPanel container = new JPanel();
  private JTextField jtf;   
  private JLabel label = new JLabel("Téléphone FR");
  private JButton b = new JButton ("OK");

  public Fenetre(){      
    this.setTitle("Animation");
    this.setSize(300, 150);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setLocationRelativeTo(null);

    container.setBackground(Color.white);
    container.setLayout(new BorderLayout());

    jtf = new JTextField();
    JPanel top = new JPanel();      

    Font police = new Font("Arial", Font.BOLD, 14);
    jtf.setFont(police);
    jtf.setPreferredSize(new Dimension(150, 30));
    jtf.setForeground(Color.BLUE);
    //On ajoute l'écouteur à notre composant
    jtf.addKeyListener(new ClavierListener());

    top.add(label);
    top.add(jtf);
    top.add(b);

    this.setContentPane(top);
    this.setVisible(true);         
  }      

  class ClavierListener implements KeyListener{

    public void keyPressed(KeyEvent event) {
      System.out.println("Code touche pressée : " + event.getKeyCode() + " - caractère touche pressée : " + event.getKeyChar());
      pause();
    }

    public void keyReleased(KeyEvent event) {
      System.out.println("Code touche relâchée : " + event.getKeyCode() + " - caractère touche relâchée : " + event.getKeyChar());         
      pause();                  
    }

    public void keyTyped(KeyEvent event) {
      System.out.println("Code touche tapée : " + event.getKeyCode() + " - caractère touche tapée : " + event.getKeyChar());
      pause();
    }   	
  }   

  private void pause(){
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }   

  public static void main(String[] args){
    new Fenetre();
  }
}

La figure suivante affiche une petite série d'essais de ce code.

Premier test de l'interface KeyListener
Premier test de l'interface KeyListener

Vous pouvez maintenant vous rendre compte de l'ordre dans lequel les événements du clavier sont gérés : en premier, lorsqu'on presse la touche, en deuxième, lorsqu'elle est tapée, et enfin, lorsqu'elle est relâchée.

Dans le cas qui nous intéresse, nous souhaitons que lorsque l'utilisateur saisit un caractère interdit, celui-ci soit automatiquement retiré de la zone de saisie. Pour cela, nous procéderons à un traitement spécifique dans la méthode keyReleased(KeyEvent event).

Si vous avez effectué beaucoup de tests de touches, vous avez dû remarquer que les codes des touches correspondant aux chiffres du pavé numérique sont compris entre 96 et 105.

À partir de là, c'est simple : il nous suffit de supprimer le caractère tapé de la zone de saisie si son code n'est pas compris dans cet intervalle. Toutefois, un problème se pose avec cette méthode : ceux qui possèdent un ordinateur portable sans pavé numérique ne pourront rien saisir alors qu'il est possible d'obtenir des chiffres en appuyant sur MAJ + &, é, ', ( ou -.

Ce souci nous amène à opter pour une autre solution : nous créerons une méthode dont le type de retour sera un booléen nous indiquant si la saisie est numérique ou non. Comment ? Tout simplement en exécutant un Integer.parseInt(value), le tout enveloppé dans un try{…}catch(NumberFormatException ex){}. Si nous essayons de convertir un caractère « a » en entier, l'exception sera levée et nous retournerons alors false (true dans le cas contraire).

Voici notre implémentation quelque peu modifiée :

class ClavierListener implements KeyListener{
  public void keyReleased(KeyEvent event) {
    if(!isNumeric(event.getKeyChar()))
      jtf.setText(jtf.getText().replace(String.valueOf(event.getKeyChar()), ""));      	
    }

    //Inutile de redéfinir ces méthodes, ous n'en avons plus l'utilité !
    public void keyPressed(KeyEvent event) {}
    public void keyTyped(KeyEvent event) {}

    //Retourne true si le paramètre est numérique, false dans le cas contraire
    private boolean isNumeric(char carac){
    try {
      Integer.parseInt(String.valueOf(carac));
    }
    catch (NumberFormatException e) {
      return false;            
    }
    return true;
  }
}

Vous vous apercevez que les lettres simples sont désormais interdites à la saisie : mission accomplie ! Cependant, les caractères spéciaux comme « ô », « ï », etc. ne sont pas pris en charge par cette méthode. Par conséquent, leur saisie reste possible.

  • L'objet JComboBox se trouve dans le package javax.swing.

  • Vous pouvez ajouter des éléments dans une liste avec la méthode addItem(Object obj).

  • Vous pouvez aussi instancier une liste avec un tableau de données.

  • L'interface ItemListener permet de gérer les états de vos éléments.

  • Vous pouvez aussi utiliser l'interface ActionListener.

  • La méthode getSelectedItem() retourne une variable de type Object : pensez donc à effectuer un cast, ou à utiliser la méthode toString() si les éléments sont des chaînes de caractères.

  • Les objets JCheckBox, JRadioButton et ButtonGroup sont présents dans le package javax.swing.

  • Vous pouvez déterminer si l'un de ces composants est sélectionné grâce à la méthode isSelected(). Cette méthode retourne true si l'objet est sélectionné, false dans le cas contraire.

  • Vous pouvez restreindre le nombre de choix à un parmi plusieurs réponses en utilisant la classe ButtonGroup.

  • Vous pouvez ajouter des boutons à un groupe de boutons grâce à la méthode add(AbstractButton button).

  • Par défaut, un JTextField accepte tous les types de caractères.

  • Un JFormattedTextField correspond, pour simplifier, à un JTextField plus restrictif.

  • On peut restreindre la saisie dans ces objets en utilisant l'objet MaskFormatter.

  • Afin de contrôler les événements clavier, l'utilisation d'une implémentation de l'interface KeyListener est nécessaire.

Example of certificate of achievement
Example of certificate of achievement