Les composants de sélection ou de choix

Chapitres traités   

Au travers de cette étude, nous nous intéressons plus particulièrement à tout ce qui nous permet de faire une sélection : un choix ou un ensemble de choix.

Nous commencerons tout simplement, même si nous l'avons largement utilisé, par un bouton (simple ou double états). Nous en profiterons pour découvrir tous les artifices qu'il est possible de mettre en oeuvre sur ces boutons, afin que l'ergonomie soit la plus intuitive, et donc la plus simple, tout en ayant une apparence des plus sympatiques.

Vous savez maintenant comment recueillir du texte tapé par des utilisateurs. Toutefois, il est souvent préférable de leur offrir un ensemble fini de choix plutôt que de leur demander d'introduire des données dans un composant de texte. Au moyen d'un ensemble de boutons ou d'une liste d'options, vous pouvez indiquer aux utilisateurs les choix mis à leur disposition. De plus, vous n'aurez pas besoin de créer des procédures de contrôle d'erreurs pour ces types de sélections. Dans cette section, vous apprendrez à programmer des cases à cocher, des boutons radio, des listes d'options, des curseurs, etc.

Choix du chapitre La classe de base AbstractButton

La classe AbstractButton est une classe abstraite et sert de super-classe pour tous les composants Swing dont le comportement est similaire à celui d'un bouton. Comme cette classe est générique, elle définit un grand nombre de propriétés. Les boutons Swing, comme les libellés, peuvent afficher du texte, une image (une icône) ou les deux.

Par défaut, un AbstractButton affiche une seule ligne de texte dans une fonte unique. Cependant, si la propriété du texte commence par "<html> ...", le bouton de texte est formaté en HTML et peut contenir plusieurs fontes et plusieurs lignes.

Gestion possible de plusieurs icônes

Les boutons Swing peuvent même afficher des icônes différentes suivant l'état dans lesquels ils se trouvent. Ainsi, en plus de l'icône par défaut, AbstractButton possède des propriétés qui spécifient les icônes à afficher :

  1. Quand le bouton est appuyé,
  2. Quand le bouton est sélectionné,
  3. Quand le bouton est désactivé,
  4. Quand le bouton est désactivé et sélectionné,
  5. Quand le curseur de la souris passe au dessus du bouton sans qu'il soit sélectionné,
  6. Et quand le curseur de la souris passe au dessus du bouton en étant sélectionné.

Si la propriété rollOver est spécifiée et si la propriété rollOverEnabled est à true (mode par défaut), la rolloverIcon est affichée quand la souris se trouve au dessus du bouton.

Gestion des icônes de la classe AbstractButton
Icon getDisabledIcon()
void setDisabledIcon(Icon icône)
Choix de l'icône du bouton lorsque ce dernier est désactivé.
Icon getDisabledSelectedIcon()
void setDisabledSelectedIcon(Icon icône)
Choix de l'icône du bouton lorsque ce dernier est sélectionné et désactivé.
Icon getIcon()
void setIcon(Icon icône)
Choix de l'icône par défaut.
Icon getPressedIcon()
void setPressedIcon(Icon icône)
Choix de l'icône du bouton lorsque ce dernier est appuyé.
Icon getRolloverIcon()
void setRolloverIcon(Icon icône)
Choix de l'icône du bouton lorsque le curseur de la souris passe au dessus sans qu'il soit sélectionné.
Icon getRolloverSelectedIcon()
void setRolloverSelectedIcon(Icon icône)
Choix de l'icône du bouton lorsque le curseur de la souris passe au dessus en étant sélectionné.
Icon getSelectedIcon()
void setSelectedIcon(Icon icône)
Choix de l'icône du bouton lorsque ce dernier est sélectionné.
boolean isRolloverSelectedIcon()
void setRolloverSelectedIcon(boolean validation)
Active ou pas le mode de gestion de passage de la souris au dessus du bouton.

Les événements

Les boutons Swing génèrent trois types d'événements :

  1. java.awt.event.ActionEvent : est généré lorsqu'un bouton quelconque est pressé.
  2. java.awt.event.ItemEvent : est généré lorsqu'un bouton de type bascule (double état) est sélectionné ou désélectionné.
  3. java.awt.event.ChangeEvent : est généré quand l'état interne du bouton change, par exemple, quand le pointeur de la souris arrive sur le bouton ou quand l'utilisateur arme le bouton en cliquant dessus.
Enregistrement des écouteurs d'événements
void addActionListener(java.awt.event.ActionListener écouteur)
void removeActionListener(java.awt.event.ActionListener écouteur)
Prise en compte ou non de la gestion d'événement de type ActionEvent.
void addChangeListener(java.awt.event.ActionListener écouteur)
void removeChangeListener(java.awt.event.ActionListener écouteur)
Prise en compte ou non de la gestion d'événement de type ChangeEvent.
void addItemListener(java.awt.event.ActionListener écouteur)
void removeItemListener(java.awt.event.ActionListener écouteur)
Prise en compte ou non de la gestion d'événement de type ItemEvent.

Plusieurs artifices d'affichage suivant l'état du bouton, et suivants ses propriétés intrinsèques

Il est possible de gérer finement l'apparence de vos boutons, surtout suivant l'état dans lequel ils se trouvent. Voici ci-dessous quelques unes des propriétés qui s'avèrent intéressantes :

Propriétés intéressantes
int getHorizontalAlignment()
void setHorizontalAlignment(int justification)
int getVerticalAlignment()
void setVerticalAlignment(int justification)
Positionnement souhaité pour l'icône et le texte. Par défaut, les éléments sont placés au centre.
int getHorizontalTextPosition()
void setHorizontalTextPosition(int justification)
int getVerticalTextPosition()
void setVerticalTextPosition(int justification)
Position du texte par rapport à l'icône.
void setEnabled(boolean activation)
Un bouton Swing peut être activé et désactivé. Les boutons désactivés sont typiquement affichés avec des graphismes grisés, bien que nous puissions spécifier une autre icône de désactivation.
int getMnemonic()
void setMnemonic(int raccourci)
void setMnemonic(char raccourci)
Nous pouvons spécifier un mnémonique. Le caractère mnémonique est alors souligné dans le texte du bouton et le bouton peut alors être pressé depuis le clavier.
String getText()
void setText(String libellé)
Introduction du libellé du bouton.
boolean isFocusPainted()
void setFocusPainted(boolean activation)
Permet de tracer ou pas le focus du bouton qui se traduit par un rectangle de couleur assez neutre autour du libellé. Le tracé est proposé par défaut. Lorsque vous utilisez plusieurs boutons, ce tracé est intéressant puisqu'il permet de voir tout de suite le bouton qui est actif. Vous pouvez alors agir avec le clavier pour lancer l'action souhaitée sans passer nécessairement par la souris. Par contre, lorsque vous possédez un seul bouton sur votre IHM, il est plutôt souhaitable de désactivé ce tracé qui devient alors gênant.
boolean isSelected()
void setSelected(boolean sélection)
Détermine si le bouton est sélectionné. Cette propriété peut s'avérer intéressante dans le cas d'une gestion de plusieurs boutons pour un même groupe, notamment pour faire un choix parmi plusieurs valeurs possibles.
void doClick()
void doClick(int delai)
Simule l'action d'un clic de la souris. Ainsi, par programme, vous pouvez lancer la méthode actionPerformed() indirectement au travers de cette méthode doClic(). Il est également possible de préciser un temps en milliseconde.
java.awt.Insets getMargin()
void setMargin(java.awt.Insets libellé)
Il possible de régler une marge intérieure, donc entre le bord du bouton et le texte du libellé.
protected void paintBorder(java.awt.Graphics contexteGraphique)
Si vous redéfinissez cette méthode, il est possible de prévoir le dessin de la bordure totalement personnalisé.
ButtonModel getModel()
void setModel(ButtonModel modèleDeBouton)
Les boutons respecte le modèle MVC (Modèle-Vue-Contrôleur). Il peut être intéressant de récupérer le modèle du bouton (chacun en dispose du sien) pour connaître l'état du bouton en un instant donné (voir plus loin).
int getIconTextGap()
void setIconTextGap(int espacement)
Définit l'espacement entre l'icône et le libellé dans le cas où ces deux éléments sont sollicités pour définir le bouton concerné.

Le Modèle-Vue-Contrôleur des boutons Swing

Nous connaissons déjà le principe du modèle MVC. Nous l'avons largement utilisé lors de l'étude précédente. Pour la plupart des composants, la classe modèle implémente une interface dont le nom se termine par Model, d'où l'interface appelée ButtonModel dans le cas des boutons.

Les classes implémentant cette interface ButtonModel peuvent ainsi définir l'état des divers types de boutons. En fait, les boutons ne sont pas aussi complexes et la bibliothèque Swing contient une unique classe appelée DefaultButtonModel qui implémente cette interface.

interface ButtonModel
String getActionCommand()
void setActionCommand(String commande)
Renvoie la chaîne de commande d'action associée à ce bouton. Par défaut, il s'agit tout simplement du libellé du bouton. Toutefois, Il est possible choisir une chaîne de commande et ainsi de préciser l'action désirée.
int getMnemonic()
void setMnemonic(char raccourci)
Le raccourci clavier pour ce bouton.
boolean isArmed()
void setArmed(boolean activation)
Précise si le bouton est pressé et que la souris se trouve toujours au-dessus.
boolean isEnabled()
void setEnabled(boolean activation)
Indique si le bouton peut être sélectionné.
boolean isSelected()
void setSelected(boolean sélection)
Indique si l'état du bouton a été basculé (pour les cases à cocher, les boutons radios et les bouton à deux états).
boolean isPressed()
void setPressed(boolean sélection)
Précise si le bouton de commande a été pressé, mais que le bouton de la souris n'a pas encore été relâché.
boolean isRollover()
void setRollover(boolean sélection)
Précise tout simplement si la souris se trouve au dessus du bouton.

 

Choix du chapitre Le bouton poussoir JButton

La classe abstraite AbstractButton est la classe de base de l'ensemble des composants constituant une sélection ou un choix. Elle possède de nombreuses compétences que nous allons illustrer au travers de notre premier composant concret, celui que nous connaissons déjà bien, JButton.

Cette classe JButton implémente un bouton poussoir. La plupart des proriétés et des méthodes intéressantes sont implémentées par la classe abstraite AbstractButton. Les boutons Swing peuvent comporter une image en plus d'un label. La classe JButton possède des constructeurs acceptant un objet Icon capable de se dessiner lui-même. Vous pouvez créer des boutons dotés de légendes, d'images ou des deux.

JButton()
JButton(String libellé)
JButton(Icon icône)
JButton(String libellé,
Icon icône)
JButton(Action action)

Il est possible de prérégler un bouton au travers d'une action. Nous avons déjà étudié ce concept lors de l'étude de la gestion des événements. Repportez-vous y pour avoir plus de précision.

Comme nous l'avons découvert dans l'étude des fenêtres, la classe conviviale ImageIcon s'occupe de charger une image pour vous et peut être utilisée pour ajouter une image à un bouton.

Mise en oeuvre de notre première application qui prend en compte certaines spécificités des boutons

Nous allons mettre en oeuvre l'application que nous connaissons bien qui permet de faire une conversion entre les €uros et les francs. Sur le bouton qui permet de réaliser la conversion, je propose cette fois-ci deux icônes :

  1. la première est l'icône par défaut qui apparaît dès le départ,
  2. la deuxième apparaît juste au moment où la souris se déplace au-dessus du bouton.

Vous avez ci-dessous les différents états du bouton de conversion :

  1. Etat désactivé :


  2. Etat normal (apparaît dès le début de la saisie sur la zone de gauche) :


  3. Survol de la souris au-dessus du bouton :


Vous remarquez la présence d'un raccourci clavier sur le caractère 'C' du bouton. Le raccourci clavier correspondant est donc <Alt+C>.
.

Code correspondant
package boutons;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.event.*;

public class Conversion extends JFrame {
   private FormatNombre saisie = new FormatNombre(NumberFormat.getCurrencyInstance());
   private FormatNombre résultat = new FormatNombre(new DecimalFormat("#,##0.00 F"));
   private Bouton validation = new Bouton();
   
   public Conversion() {
      super("Conversion €uro -> Francs");
      setLayout(new FlowLayout());
      add(saisie);
      add(validation);
      résultat.setEditable(false);
      add(résultat);
      saisie.addKeyListener(new KeyAdapter() {
         @Override
         public void keyTyped(KeyEvent ev) {
            validation.setEnabled(true);
         }
       });      
      getContentPane().setBackground(Color.YELLOW);
      pack();
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private class FormatNombre extends JFormattedTextField {
      public FormatNombre(NumberFormat format) {
         super(format);
         setColumns(12);
         setValue(0);
         setHorizontalAlignment(RIGHT);
         setMargin(new Insets(3, 3, 3, 3));
      }    
   }
   
   private class Bouton extends JButton  implements ActionListener, ChangeListener {
      public Bouton() {
         super("Convertion", new ImageIcon("validation.gif"));
         addActionListener(this);
         addChangeListener(this);
         setEnabled(false);
         setFocusPainted(false);
         setRolloverIcon(new ImageIcon("survol.gif"));
         setMnemonic('C');
      }    
      
      public void actionPerformed(ActionEvent e) {
        final double TAUX = 6.55957;
        double €uro = ((Number)saisie.getValue()).doubleValue();
        double franc = €uro * TAUX;
        résultat.setValue(franc);
      }

      public void stateChanged(ChangeEvent e) {
         setForeground(getModel().isRollover() ? Color.RED : Color.BLACK);
      }
   }
   
   public static void main(String[] args)  { new  Conversion(); }   
}
Il est possible, comme nous l'avons évoqué, de préciser le libellé du bouton en format HTML afin d'obtenir la visualisation souhaitée. Dans ce cas là, même si nous sollicitons un raccourci clavier, le symbole du raccourci n'apparaît pas sous la lettre 'C'.

package boutons;
...

public class Conversion extends JFrame {  
...
   private class Bouton extends JButton  implements ActionListener, ChangeListener {
      public Bouton() {
         super("<html><i>Convertion<br>€uro => Franc</i></html>", new ImageIcon("validation.gif"));
         ...
      }    
...
}

 

Choix du chapitre Bouton à bascule JToggleButton

La classe JToggleButton implémente un bouton à bascule : un bouton qui peut être sélectionné ou déselectionné. L'utilisateur peut basculer entre les deux états en cliquant sur le bouton. Comme tous les boutons Swing, un JToggleButton peut afficher du texte et une icône.

L'état de sélection est généralement indiqué par la bordure du bouton et sa couleur de fond. Nous pouvons aussi appeler les méthodes setIcon() et setSelectedIcon() pour spécifier diverses icônes pour les états standard et sélectionné. Comme pour JButton, la classe JToogleButton possède des constructeurs acceptant un objet Icon capable de se dessiner lui-même. Vous pouvez créer des boutons dotés de légendes, d'images ou des deux. Il est également possible de faire en sorte que le bouton soit dans l'état sélectionné dès le départ.

JToggleButton()
JToggleButton(Action action)
JToggleButton(String libellé)
JToggleButton(Icon icône)
JToggleButton(String libellé,
Icon icône)
JToggleButton(String libellé, boolean sélectionné)
JToggleButton(Icon icône, boolean sélectionné)
JToggleButton(String libellé,
Icon icône, boolean sélectionné)

Il est possible de prérégler un bouton au travers d'une action. Nous avons déjà étudié ce concept lors de l'étude de la gestion des événements. Repportez-vous y pour avoir plus de précision.

Par défaut, JToggleButton conserve la trace de son état de sélection avec un objet JToogleButton.ToggleButtonModel. Cette classe JToogleButton.ToggleButtonModel est le bouton ButtonModel utilisé par défaut par les composants JToogleButton, JCheckBox et JRadioButton. Elle surcharge plusieurs méthodes de DefaultButtonModel pour déléguer les informations d'état de sélection du bouton à un objet ButtonGroup (voir plus loin). Les applications n'ont généralement pas besoin d'instancier cette classe.

JToggleButton est moins couramment utilisé que ses sous-classes JCheckBox et JRadioButton.
.

Exploitation de l'action sur un bouton à bascule

Nous aurons souvent besoin d'exploiter un bouton à bascule de deux façons différentes :

  1. En réagissant immédiatement à une action directe sur le bouton,
  2. En cherchant à connaître son état à un instant donné.

Retour sur l'application de conversion

Nous allons permettre à l'application précédente de convertir dans les deux sens. Nous rajoutons pour cela un bouton à bascule qui permet d'effectuer le choix désiré.

Vous avez ci-dessous les deux états possibles correspond à la conversion désirée :

  1. Etat activé permettant la conversion des €uros vers les Francs :


  2. Etat désactivé permettant la conversion des Francs vers les €uros :


Code correspondant
package boutons;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.event.*;

public class Conversion extends JFrame {
   private FormatNombre  = new FormatNombre(NumberFormat.getCurrencyInstance());
   private FormatNombre F = new FormatNombre(new DecimalFormat("#,##0.00 F"));
   private Bouton validation = new Bouton();
   private Bascule choix = new Bascule();
   
   public Conversion() {
      super("Conversion €uro <-> Francs");
      setLayout(new FlowLayout());
      add();
      add(validation);
      add(choix);
      add(F);    
      getContentPane().setBackground(Color.YELLOW);
      pack();
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private class FormatNombre extends JFormattedTextField {
      public FormatNombre(NumberFormat format) {
         super(format);
         setColumns(12);
         setValue(0);
         setHorizontalAlignment(RIGHT);
         setMargin(new Insets(3, 3, 3, 3));
      }    
   }
   
   private class Bouton extends JButton  implements ActionListener, ChangeListener {
      public Bouton() {
         super("Convertion", new ImageIcon("validation.gif"));
         addActionListener(this);
         addChangeListener(this);
         setFocusPainted(false);
         setRolloverIcon(new ImageIcon("survol.gif"));
         setMnemonic('C');
      }    
      
      public void actionPerformed(ActionEvent e) {
        final double TAUX = 6.55957;
        if (choix.isSelected()) {
           double €uro = ((Number).getValue()).doubleValue();
           double franc = €uro * TAUX;
           F.setValue(franc);
        }
        else {
           double franc = ((Number)F.getValue()).doubleValue();
           double euro = franc / TAUX;
           .setValue(euro);           
        }
      }

      public void stateChanged(ChangeEvent e) {
         setForeground(getModel().isRollover() ? Color.RED : Color.BLACK);
      }
   }
   
   private class Bascule extends JToggleButton implements ActionListener {
      public Bascule() {
         super("€ => F",  new ImageIcon("validation.gif"), true);
         setHorizontalTextPosition(LEFT);
         setFocusPainted(false);
         addActionListener(this);
      }
      
      public void actionPerformed(ActionEvent e) {
         setText(isSelected() ? " € => F" : "€ <= F");
      }      
   }
   
   public static void main(String[] args)  { new  Conversion(); }   
}

Autre application qui permet de choisir la taille d'une image

Dans l'exemple ci-dessous, lorsque l'image apparaît sur la zone principale de la fenêtre, nous pouvons, à l'aide d'un bouton à bascule, choisir si elle doit s'afficher en taille réelle ou au contraire si elle doit être totalement visible quelque soit la dimension de la fenêtre.

Code correspondant



package cadre;

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

public class Fenêtre extends JFrame implements ActionListener {
   private JLabel bienvenue = new JLabel("Bienvenue...");
   private JToggleButton changement = new JToggleButton("Dimensions image normale");
   private PanneauImage panneauImage = new PanneauImage();
   private JPanel panneauSud = new JPanel();
   private JPanel panneauCentre = new JPanel();
   
   private String metal = "javax.swing.plaf.metal.MetalLookAndFeel";
   private String motif = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
   private String windows = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
   private String windowsClassic = "com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel";
   
   public Fenêtre() throws Exception {
      setTitle("Transparence");
      setBounds(100, 100, 400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      
      UIManager.setLookAndFeel(motif);
      
      bienvenue.setFont(new Font("Arial", Font.ITALIC+Font.BOLD, 54));
      bienvenue.setForeground(new Color(0, 255, 0, 96));
      
      panneauCentre.setOpaque(false);
      panneauCentre.add(bienvenue);
      
      changement.addActionListener(this);
      changement.setOpaque(false);
      changement.setFocusPainted(false);
      changement.setForeground(Color.YELLOW);
      
      panneauSud.setOpaque(false);
      panneauSud.add(changement);
      
      panneauImage.setLayout(new BorderLayout());
      panneauImage.add(panneauCentre);
      panneauImage.add(panneauSud, BorderLayout.SOUTH);
      
      add(panneauImage);

      setVisible(true);
   }
    
   public static void main(String[] args) throws Exception {
      new Fenêtre()  ;
   }

   public void actionPerformed(ActionEvent e) {
      if (changement.isSelected()) {
        panneauImage.dimensionAutomatique = false;
        changement.setText("Taille adaptée à la fenêtre");
      }
      else {
        panneauImage.dimensionAutomatique = true;
        changement.setText("Dimensions image normale");          
      }
      panneauImage.repaint();
   }
}

class PanneauImage extends JComponent {
    boolean dimensionAutomatique = true;
    private Image imageFond = new ImageIcon("Cabane dans un champ.jpg").getImage();
    
   @Override
    public void paintComponent(Graphics fond) {
        if (dimensionAutomatique)
           fond.drawImage(imageFond, 0, 0, getWidth(), getHeight(), null);
        else
           fond.drawImage(imageFond, 0, 0, imageFond.getWidth(null), imageFond.getHeight(null), null);
    }
}

 

Choix du chapitre Le groupe de boutons ButtonGroup

Il peut être intéressant de regrouper un ensemble de boutons à bascule ou de boutons radio, de telle sorte que lorsque un bouton est sélectionné, les autres se retrouvent automatiquement désactivés. Les boutons s'excluent alors réciproquement. La classe ButtonGroup permet de réaliser cette fonction et impose une exclusion mutuelle (un comportement de bouton radio) à un groupe de boutons. Une fois les boutons ajoutés à un ButtonGroup par la méthode add(), l'exclusion mutuelle est automatique, sans qu'aucune action ultérieure soit nécessaire.

L'objet ButtonGroup est très ordinaire. On pourrait s'attendre à ce qu'il soit un conteneur ou un composant, amis il n'en est rien ; il s'agit simplement d'un objet assistant qui n'autorise le choix que d'un seul bouton à la fois.

La classe ButtonGroup possède une méthode getSelection() qui renvoie une référence au modèle de bouton (ButtonModel), mais pas le bouton lui-même. L'interface ButtonModel possède à son tour la méthode getActionCommand() qui renvoie alors la commande d'action d'un bouton correspondant au libellé du bouton.

Retour sur l'application de conversion

Cette fois-ci, prévoyons deux boutons pour choisir la conversion désirée, un pour les Francs (activé par défaut) et l'autre pour les €uros. Lorsqu'un bouton est sélectionné l'autre se désactive automatiquement.

Code correspondant

package boutons;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.event.*;

public class Conversion extends JFrame {
   private FormatNombre  = new FormatNombre(NumberFormat.getCurrencyInstance());
   private FormatNombre F = new FormatNombre(new DecimalFormat("#,##0.00 F"));
   private Bouton conversion = new Bouton();
   private ButtonGroup groupe = new ButtonGroup();
   private Bascule choix€ = new Bascule("");
   private Bascule choixF = new Bascule("F");   
   
   public Conversion() {
      super("Conversion €uro <-> Francs");
      setLayout(new FlowLayout());
      add();
      add(conversion);
      add(choix€);
      add(choixF);
      add(F);    
      choixF.setSelected(true);
      getContentPane().setBackground(Color.YELLOW);
      pack();
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private class FormatNombre extends JFormattedTextField {
      public FormatNombre(NumberFormat format) {
         super(format);
         setColumns(12);
         setValue(0);
         setHorizontalAlignment(RIGHT);
         setMargin(new Insets(3, 3, 3, 3));
      }    
   }
   
   private class Bouton extends JButton  implements ActionListener, ChangeListener {
      public Bouton() {
         super("Convertion", new ImageIcon("validation.gif"));
         addActionListener(this);
         addChangeListener(this);
         setFocusPainted(false);
         setRolloverIcon(new ImageIcon("survol.gif"));
         setMnemonic('C');
      }    
      
      public void actionPerformed(ActionEvent e) {
        final double TAUX = 6.55957;
        if (choixF.isSelected()) {
           double €uro = ((Number).getValue()).doubleValue();
           double franc = €uro * TAUX;
           F.setValue(franc);
        }
        else {
           double franc = ((Number)F.getValue()).doubleValue();
           double euro = franc / TAUX;
           .setValue(euro);           
        }
      }

      public void stateChanged(ChangeEvent e) {
         setForeground(getModel().isRollover() ? Color.RED : Color.BLACK);
      }
   }
   
   private class Bascule extends JToggleButton implements ItemListener {
      public Bascule(String libellé) {
         super(libellé);
         setFocusPainted(false);
         groupe.add(this);
         addItemListener(this);
      }

      public void itemStateChanged(ItemEvent e) {
          setForeground(isSelected() ? Color.RED : Color.BLACK);
      }
   }
   
   public static void main(String[] args)  { new  Conversion(); }   
}

Retour sur les événements : La classe abstraite AbstractButton dispose déjà de tous les ingrédients pour gérer correctement tous les types d'événements. Ainsi, les boutons, qui héritent tous de cette classe de base, récupèrent automatiquement tout ce potentiel.

Jusqu'à présent, nous avons surtout utilisé l'événement de type Action. Dans l'exemple qui précéde, et plus généralement lorsque nous avons une gestion globale des boutons à prendre en compte, grâce à la notion de groupe, l'événement de type Item prend alors toute son importance. Je rappelle que cet événement est sollicité à chaque fois qu'un bouton change d'état, qu'il soit sélectionneé directement ou pas. Ainsi, il est possible d'agir comme dans cet exemple, en temps réel, sur la couleur des libellés des boutons.

 

Choix du chapitre Les cases à cocher JCheckBox

Un petit peu à l'image des boutons à bascule, si vous souhaitez recueillir une réponse qui se limite à une entrée "oui" ou "non", utilisez plutôt une case à cocher. Ce type de composant, qui est représenté par la classe JCheckBox, s'accompagne d'un intitulé qui permet de les identifier. L'utilisateur clique à l'intérieur de la case pour la cocher, et fait de même pour la désactiver.

Pour ajouter/supprimer la coche, l'utilisateur peut également appuyer sur la barre d'espace si le focus d'entrée se trouve dans la case à cocher.
.

L'action de l'utilisateur sur une case à cocher se limite à la modification de sont état : passage de l'état coché à l'état non coché, ou l'inverse. L'état visuel de la case (cochée, non cochée) est gérée automatiquement par les méthodes de la classe JCheckBox. Lorsque nous cochons une case, son état n'est généralement contrôlé qu'ultérieurement, par exemple au moment où l'utilisateur lance une action. Cela ressemble à un peu à un formulaire : nous pouvons mofifier nos choix tant que nous ne l'avons pas envoyé.

Les cases à cocher s'accompagnent d'un libellé qui indique leur rôle. Le texte prévu pour le libellé est passé au constructeur. Par défaut, une case à cocher est construite dans l'état non coché. Nous pouvons lui imposer l'état coché en utilisant une autre version de constructeur.

Comme pour les autres boutons, il est également possible de prévoir une icône à la place du libellé ou avec le libellé. Attention, dans ce cas là, l'image (l'icône) représentant la case à cocher n'existe plus.

Les constructeurs suivant vous offrent toutes les opportunités souhaités :

JCheckBox()
JCheckBox(Action action)
JCheckBox(String libellé)
JCheckBox(Icon icône)
JCheckBox(String libellé,
Icon icône)
JCheckBox(String libellé, boolean sélectionné)
JCheckBox(Icon icône, boolean sélectionné)
JCheckBox(String libellé,
Icon icône, boolean sélectionné)

Il est possible de prérégler un bouton au travers d'une action. Nous avons déjà étudié ce concept lors de l'étude de la gestion des événements. Repportez-vous y pour avoir plus de précision.

Après coup, et indépendamment de l'action de l'utilisateur, nous pouvons, à tout instant, imposer par programme un état donné à l'aide de la méthode setSelected().

Un tel appel de méthode, comme la classe ToogleButton, génère un événement de type Item.
.

De la même façon, nous pouvons connaître à tout instant l'état d'une case à cocher (indépendamment de son éventuel changement d'état provoqué par l'utilisateur) à l'aide de la méthode isSelected().

Comme tout composant, nous ajoutons une case à cocher à un conteneur par la méthode add() de ce dernier.

Retour sur les événements : La classe abstraite AbstractButton dispose déjà de tous les ingrédients pour gérer correctement tous les types d'événements. Ainsi, les boutons, qui héritent tous de cette classe de base, récupèrent automatiquement tout ce potentiel.

Ici aussi, comme pour la classe ToggleButton, nous pouvons aussi bien prendre un événement de type Action qu'un événement de type Item. Par contre, d'après ce que nous avons dit plus haut, beaucoup d'applications ne s'en préoccupent pas, puisque c'est plutôt lors d'une validation par un bouton classique (JButton) et au travers de la méthode isSelected() que l'état des cases à cocher sont réellement identifiées. Ainsi, généralement, la gestion des événements est plutôt différée.

Application sur une horloge numérique

Afin d'illustrer le comportement des cases à cocher, je vous propose de revenir sur une application que nous avons déjà mise en oeuvre. Il s'agit d'une horloge avec un simple affichage numérique. L'affichage de la police de caractères est réglable. Il est possible de demander à avoir les chiffres en italiques et/ou en gras. Dans cette application, dès qu'une case à cocher est sollicité, le changement d'aspect de la police est instantanément répercuté. Pour cela, je prévois une gestion d'événement de type Item.

Code correspondant

package horloge;

import java.awt.*;
import java.awt.event.*;
import java.text.DateFormat;
import java.util.Date;
import javax.swing.*;
import javax.swing.border.*;

public class Boutons extends JFrame implements ActionListener {
   private Timer minuteur = new Timer(1000, this);
   private JLabel heure = new JLabel();
   private CaseACocher caseGras = new CaseACocher("Gras");
   private CaseACocher caseItalique = new CaseACocher("Italique");
   private JPanel panneau = new JPanel();
   
   public Boutons() {
      super("Horloge");     
      heure.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32));
      heure.setHorizontalAlignment(JLabel.CENTER);
      heure.setBorder(BorderFactory.createLoweredBevelBorder());
//      heure.setOpaque(true);
//      heure.setBackground(Color.YELLOW);
      add(heure);      
      panneau.add(caseGras);
      panneau.add(caseItalique);
      panneau.setBackground(Color.ORANGE);
      add(panneau, BorderLayout.SOUTH);
      minuteur.start();
      setSize(170, 100);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setResizable(false);
      setVisible(true);
   }   

   public void actionPerformed(ActionEvent e) {
      heure.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date()));
   }
   
   private class CaseACocher extends JCheckBox implements ItemListener {
      public CaseACocher(String libellé) {
         super(libellé, true);
         setMnemonic(libellé.charAt(0));
         addItemListener(this);
         setOpaque(false);
      }

      public void itemStateChanged(ItemEvent e) {
         int mode = 0;
         mode += caseGras.isSelected() ? Font.BOLD : 0;
         mode += caseItalique.isSelected() ? Font.ITALIC : 0;
         heure.setFont(new Font("Arial", mode, 32));
      }
   }
   
   public static void main(String[] args) {  new Boutons(); }
}

Changement des icônes d'activation et de désactivation

La représentation visuelle des cases à cocher est standard. Nous retrouvons les mêmes apparences quelque soit la technologie utilisée. Il est toutefois possible de personnaliser cet aspect visuel. Comme nous l'avons vu, il suffit de proposer une icône personnelle lorsque la case est non active et une autre lorsque la case est cochée. Vous pouvez en profiter pour en placer une autre qui correspond au passage de la souris au dessus de votre case à cocher.

Voici par exemple ce que nous pouvons obtenir à partir de l'exemple précédent :

Cette fois-ci, les libellés n'existent plus (informations redondantes). Deux icônes seulement représentent les deux cases à cocher.

  1. Dans le cas où elles sont actives, les lettres correspondant à l'action souhaitée apparaissent normalement, respectivement B et I.
  2. Lorsque qu'une case n'est plus active une croix rouge se place alors au dessus de la lettre concernée.
  3. Pour finir, si nous déplaçons le curseur de la souris au dessus de l'une des cases à cocher, la couleur de la lettre devient verte, que cette dernière soit active ou pas.
Partie de code correspondant
...

public class Boutons extends JFrame implements ActionListener {
...
   private CaseACocher caseGras = new CaseACocher("Gras");
   private CaseACocher caseItalique = new CaseACocher("Italique");
   private JPanel panneau = new JPanel();
   
...
   
   private class CaseACocher extends JCheckBox implements ItemListener {
      public CaseACocher(String libellé) {
         super(new ImageIcon(libellé+"Inactif.gif"), true);
         setSelectedIcon(new ImageIcon(libellé+".gif"));
         Icon icône = new ImageIcon(libellé+"Over.gif");
         setRolloverIcon(icône);
         setRolloverSelectedIcon(icône);
         setMnemonic(libellé.charAt(0));
         addItemListener(this);
         setOpaque(false);
      }

      public void itemStateChanged(ItemEvent e) {
         int mode = 0;
         mode += caseGras.isSelected() ? Font.BOLD : 0;
         mode += caseItalique.isSelected() ? Font.ITALIC : 0;
         heure.setFont(new Font("Arial", mode, 32));
      }
   }
   
   public static void main(String[] args) {  new Boutons(); }
}

 

Choix du chapitreLes boutons radio JRadioButton

Dans l'exemple précédent, l'utilisateur pouvait activer ou désactiver une ou plusieurs options, ou aucune. De nombreuses situations exigent que l'utilisateur ne puissent choisir qu'une seule option parmi un ensemble de choix. Lorqu'une seconde option est sélectionnée, la première est désactivée.

Cet ensemble d'options est souvent mis en oeuvre au moyen d'un groupe de boutons radio, représentés par la classe JRadioButton. Ils sont ainsi appelés, car ils fonctionnent de la même manière que les boutons d'une radio : lorsque vous appuyez sur un bouton, celui qui était enfoncé ressort.

Ainsi, comme la case à cocher, le bouton radio permet d'exprimer un choix de type "oui" ou "non". Mais sa vocation est systématiquement de faire partie d'un groupe de boutons, représenté par la classe ButtonGroup, dans lequel une seule option peut être sélectionnée à la fois. Autrement dit, et comme nous l'avons déjà vu, cette classe ButtonGroup fait en sorte que choix d'une option entraine automatiquement la désactivation de l'option choisie précédemment.

Notez qu'un groupe de boutons ne contrôle que le comportement des boutons. Si vous voulez grouper les options pour des raisons de présentation, vous devez les placer dans un conteneur tel que JPanel. Comme tout composant, nous ajoutons un bouton radio à un conteneur par la méthode add() de ce dernier.

Comme pour les cases à cocher, les boutons radio s'accompagnent d'un libellé qui indique un choix possible parmi plusieurs autres. Le texte prévu pour le libellé est passé au constructeur. Par défaut, un bouton radio est construit dans l'état non actif. Nous pouvons lui imposer l'état sélectionné en utilisant une autre version de constructeur. Si vous sollicitez plusieurs boutons radio actifs dès le départ, c'est le dernier construit qui est effectivement sélectionné, puisque normalement un seul bouton radio doit être activé.

Comme pour les autres boutons, il est également possible de prévoir une icône à la place du libellé ou avec le libellé. Attention, dans ce cas là, l'image (l'icône) représentant la case à cocher n'existe plus.

Les constructeurs suivant vous offrent toutes les opportunités souhaités :

JRadioButton()
JRadioButton(Action action)
JRadioButton(String libellé)
JRadioButton(Icon icône)
JRadioButton(String libellé,
Icon icône)
JRadioButton(String libellé, boolean sélectionné)
JRadioButton(Icon icône, boolean sélectionné)
JRadioButton(String libellé,
Icon icône, boolean sélectionné)

Les boutons radio n'ont toutefois pas la même apparence que les cases à cocher, ce qui permet de les différencier. Ces dernières se présentent en standard sous la forme d'un carré qui contient une coche une fois qu'elles sont sélectionnées. Les boutons radio sont ronds et contiennent un point lorsqu'ils sont activés.

Après coup, et indépendamment de l'action de l'utilisateur, nous pouvons, à tout instant, imposer par programme un état donné à l'aide de la méthode setSelected().

Un tel appel de méthode, comme la classe ToogleButton, génère un événement de type Item.
.

De la même façon, nous pouvons connaître à tout instant l'état d'un bouton radio (indépendamment de son éventuel changement d'état provoqué par l'utilisateur) à l'aide de la méthode isSelected().

Retour sur les événements : La classe abstraite AbstractButton dispose déjà de tous les ingrédients pour gérer correctement tous les types d'événements. Ainsi, les boutons, qui héritent tous de cette classe de base, récupèrent automatiquement tout ce potentiel.

Ici aussi, comme pour la classe ToggleButton, nous pouvons aussi bien prendre un événement de type Action qu'un événement de type Item. Par contre, d'après ce que nous avons dit plus haut, beaucoup d'applications ne s'en préoccupent pas, puisque c'est plutôt lors d'une validation par un bouton classique (JButton) et au travers de la méthode isSelected() que l'état des boutons radio sont réellement identifiées. Ainsi, généralement, la gestion des événements est plutôt différée.

Application sur l'horloge numérique

Afin d'illustrer le comportement des boutons radio, je vous propose de revenir sur l'horloge. Cette fois-ci, il est possible de choisir entre l'heure, la date ou les deux. Dans cette application, dès qu'un bouton radio est sollicité, le changement doit s'opérer instantanément. Pour cela, je prévois une gestion d'événement de type Action. Cette fois-ci, l'événement de type Item n'est pas bon choix. Effectivement seul le bouton sélectionné doit proposer l'action désirée. Ici, le changement d'état d'un bouton n'est pas intéressant, seule la sélection est utile.

Code correspondant

package horloge;

import java.awt.*;
import java.awt.event.*;
import java.text.DateFormat;
import java.util.Date;
import javax.swing.*;
import javax.swing.border.*;

public class Boutons extends JFrame implements ActionListener {
   private Timer minuteur = new Timer(1000, this);
   private JLabel heure = new JLabel();
   private ButtonGroup groupe = new ButtonGroup();
   private BoutonRadio choixHeure = new BoutonRadio("Heure");
   private BoutonRadio choixDate = new BoutonRadio("Date");
   private BoutonRadio choixLesDeux = new BoutonRadio("Les deux");
   private JPanel panneau = new JPanel();
   private DateFormat présentation = DateFormat.getTimeInstance(DateFormat.MEDIUM);
   
   public Boutons() {
      super("Horloge");     
      heure.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32));
      heure.setHorizontalAlignment(JLabel.CENTER);
      heure.setBorder(BorderFactory.createLoweredBevelBorder());
      add(heure);      
      choixHeure.setSelected(true);
      panneau.add(choixHeure);
      panneau.add(choixDate);
      panneau.add(choixLesDeux);
      panneau.setBackground(Color.ORANGE);
      add(panneau, BorderLayout.SOUTH);
      minuteur.start();
      setSize(380, 100);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setResizable(false);
      setVisible(true);
   }   

   public void actionPerformed(ActionEvent e) {
      heure.setText(présentation.format(new Date()));
   }
   
   private class BoutonRadio extends JRadioButton implements ActionListener {      
      public BoutonRadio(String libellé) {
         super(libellé);
         setMnemonic(libellé.charAt(0));
         addActionListener(this);
         setOpaque(false);
         groupe.add(this);
      }

      public void actionPerformed(ActionEvent e) {
         if (choixHeure.isSelected()) 
            présentation = DateFormat.getTimeInstance(DateFormat.MEDIUM);
         else if (choixDate.isSelected()) 
            présentation = DateFormat.getDateInstance(DateFormat.FULL);
         else if (choixLesDeux.isSelected()) 
            présentation = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM);
         heure.setText(présentation.format(new Date()));
      }
   }
   
   public static void main(String[] args) {  new Boutons(); }
}

 

Choix du chapitre Choix simple ou multiple par une liste de valeurs - JList

En java, JList est un composant qui permet de choisir une ou plusieurs valeurs dans une liste prédéfinie. Comme pour les boutons radio, les listes permettent donc à l'utilisateur d'effectuer un choix parmi plusieurs valeurs. Toutefois, c'est un seul composant qui réalise cette opération. Par ailleurs, elles peuvent être configurées de sorte qu'il soit possible d'effectuer plusieurs choix en une seule fois.

Attention : JList n'hérite pas de AbstractButton.
.

Les éléments de la liste sont généralement des chaînes de caractères. Il est possible toutefois d'utiliser n'importe quel type de composant du moment qu'il hérite justement de la classe Component. Nous pouvons ainsi avoir des éléments de type JLabel, qui permettent donc d'incorporer à la fois du texte et une icône, des éléments qui soient capable d'afficher des images, des éléments qui soient capable d'afficher des dessins, etc. Nous pouvons même envisager d'avoir une liste de chaîne de caractères et d'avoir un rendu (apparence) sous forme de composant spécifique : JLabel, images, dessins, etc.

Sélection d'un élément

La sélection d'un élément ou d'un ensemble d'éléments est classique dans un environnement graphique :

  1. L'utilisateur peut choisir un élément en cliquant dessus.
  2. Il peut étendre sa sélection à une fourchette d'élément en maintenant la touche <Maj> enfoncée tout en cliquant sur un autre élément.
  3. Pour effectuer des sélections discontinues, il utilise la touche <Ctrl> à la place de la touche <Maj>. Sur Mac, c'est la touche <Commande>.

Création d'une boîte de liste

Nous pouvons créer des boîtes de liste à l'aide du constructeur adapté à la situation requise :

  1. JList() : Préparer une liste vide qui sera complété plus tard à la suite d'événements spécifiques à l'aide de la méthode setListData().
  2. JList(Object[] liste) : Construire la boîte au moyen d'un tableau d'objet, comme par exemple un tableau de chaînes de caractères : String[]. Dans ce cas de figure, le nombre d'élément est prédéterminé.
  3. JList(Vector<?> liste) : Nous pouvons aussi construire la boîte de liste au moyen d'un tableau dynamique représenté par la classe Vector<?>. Cette fois-ci, le nombre d'élément n'est pas spécialement fixé.
  4. JList(ListModel modèleDeListe) : La classe JList est conçue, comme les autres, suivant le modèle MVC. Ainsi, il est possible de construire une liste par rapport à une autre en connectant les modèles entre eux au moyen de la méthode getModel() de la première liste déjà construite.

Choix du type de sélection

Il existe trois sortes de boîtes de liste, correspondant chacune à la façon de sélectionner un élément ou un ensemble d'éléments. Il est donc nécessaire, une fois que l'objet JList est construit, de choisir le mode de sélection au travers de la méthode setSelectionMode() en proposant le type approprié délivré par la classe ListSelectionModel :

  1. SINGLE_SELECTION : Sélection d'une seule valeur.
  2. SINGLE_INTERVAL_SELECTION : Sélection d'une seule plage de valeur (contiguës).
  3. MULTIPLE_INTERVAL_SELECTION : Sélection d'un nombre quelconque de plages de valeurs.

Par défaut, nous avons affaire à une boîte de liste de type MULTIPLE_INTERVAL_SELECTION.
.

Pour sélectionner une plage de valeur, comme nous l'avons évoqué plus haut, l'utilisateur doit cliquer sur la première, appuyer sur la touche <Maj> et, tout en la maintenant enfoncée, cliquer sur la dernière valeur de la plage. Pour sélectionner plusieurs plages, il doit procéder de même, tout en maintenant en outre la touche <Ctrl> enfoncée.

Apparence des boîtes de liste

Par défaut, une boîte de liste affiche toutes les options présentes dans la liste dans la mesure de la capacité du conteneur, ce qui la différencie de la boîte combo qui elle, n'affiche qu'une option à la fois (au repos). Bien entendu, nous pouvons modifier ce comportement initial pour que la visualisation de votre liste corresponde à l'apprence souhaitée.

  1. Injection d'une barre de défilement : Il est possible que le nombre d'option soit très grand, et du coup, les dernières ne seront alors plus visibles, ce qui implique que vous ne pourrez plus les atteindre. Pour avoir accès à l'ensemble des options, pensez à proposer un panneau de défilement intermédiaire de type JScrollPane.
  2. Définir un nombre d'élément restraint à afficher : Par défaut, dans la mesure de la capacité du conteneur, un grand nombre d'élément sont directement visibles. Il est possible d'imposer un nombre restraint d'éléments à afficher en même temps, par exemple 3, au moyen de la méthode setVisibleRowCount().
  3. Choisir l'orientation de la liste ou présentation sous forme de tableau : En standard, la boîte de liste s'affiche verticalement, c'est-à-dire que l'ensemble des éléments de la liste s'affiche les uns au dessus des autres. Il est possible de changer cette présentation au travers de la méthode setLayoutOrientation() en proposant la bonne orientation souhaitée, en conjonction éventuellement de la méthode setVisibleRowCount() que nous venons de voir. La classe JList dispose de trois constantes spécifiques à l'orientation : VERTICAL, HORIZONTAL_WRAP et VERTICAL_WRAP :


  4. Influencer le rendu de chaque cellule : La boîte de liste est capable d'afficher directement des chaînes de caractères, ce qui est bien entendu tout à fait normal. Par défaut, c'est d'ailleurs son seul mode de présentation, avec éventuellement les icônes (si elles ne sont pas trop grande). Dans ce dernier cas, il faut utiliser la classe Icon. Aussi, si vous devez afficher, pour chaque cellule, autre chose que ces deux types d'éléments, il faudra lui expliquer comment faire. Cela se fait au travers de la méthode setCellRenderer(). En argument de cette méthode, vous devez alors spécifier un objet qui implémente l'interface ListCellRenderer. Cette interface possède une seule méthode getListCellRendererComponent() que vous êtes donc obligé de redéfinir afin d'indiquer comment doit se faire la présentation souhaités. Voici la signature de cette méthode :

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus);

    Remarquez bien au passage que cette méthode retourne un Component. Cela signifie que votre objet qui implémente cette interface ListCellRenderer doit également hérité d'une classe enfant de Component.
  5. Imposer des dimensions aux cellules de la liste : Toujours dans la présentation des cellules, il est possible d'imposer une largeur ou une hauteur, au travers respectivement des méthodes setFixedCellWidth() et setFixedCellHeight().
  6. Proposer une couleur particulière pour la sélection : Il est possible de choisir la couleur de fond et la couleur du texte des éléments sélectionnés au travers des méthodes setSelectionBackground() et setSelectionForeground().

Imposer une sélection

Initialement, aucune valeur n'est sélectionnée dans la liste. Le cas échéant, nous pouvons forcer la sélection :

  1. d'un élément de rang donné par la méthode setSelectedIndex(int).
  2. d'un ensemble d'éléments en proposant les indices concernés au travers d'un tableau et avec la méthode setSelectedIndices(int[]).
  3. d'une valeur particulière au travers de la méthode setSelectedValue(Object, boolean). La valeur booléenne passée en argument, si elle est valide, demande un déplacement de l'ascenceur pour que la sélection devienne visible dans la boîte de liste.
  4. d'un ensemble d'éléments délimité par un inteval d'indices au travers de la méthode setSelectionInterval(int, int).

Accès aux informations sélectionnées

Bien entendu, si nous avons une liste de choix, c'est ultérieurement pour récupérer les valeurs sélectionnées afin de réaliser le traitement souhaité :

  1. Pour une liste à sélection simple, la méthode getSelectedValue() fournit la (seule) chaîne de caractères sélectionnée. On notera que son résultat est de type Object et non String. Il faudra donc procéder à une conversion explicite.
  2. Pour les autres types de liste, la méthode getSelectedValue() reste utilisable, mais elle fournit la première des valeurs sélectionnées. Pour obtenir toutes les valeurs, nous utiliserons généralement la méthode getSelectedValues() qui fournit un tableau d'objets. Là encore, une conversion en chaîne de caractères sera nécessaire pour chacun des objets sélectionnés.
  3. A la place des valeurs, il est possible de récupérer plutôt leurs position dans la liste au moyen des méthodes getSelectedIndex() et getSelectedIndices().

Evénements générés par les boîtes de liste

Contrairement à d'autres composant de choix, la boîte de liste ne génère pas d'événement Action. Il faut dire à ce sujet que JList n'hérite pas d'AbstractButton. Dans certains cas, nous pourrons nous contenter d'accéder à l'information sélectionnée sur une action externe à la boîte de liste, par exemple :

  1. à la fermeture d'une boîte de dialogue contenant la boîte.
  2. sur l'action d'un bouton associé à la liste, servant à valider la sélection.

Dans d'autres cas, il faudra intercepter les événements générés par la liste elle-même. Ils sont de la catégorie ListSelection, laquelle ne comporte qu'un seul type d'événement. Nous le traitons par un écouteur approprié, c'est-à-dire d'une classe implémentant l'interface ListSelectionListener (elle figure dans le paquetage swing.event) qui comporte une seule méthode valueChanged().

Mais, assez curieusement, ces événements sont générés plus souvent que nous le souhaiterions pour une bonne gestion de la liste. En effet, même dans un cas d'une liste à sélection simple, nous les obtenons :

  1. lors d'un appui sur le bouton de la souris car il correspond à la désélection de la valeur précédente,
  2. lors du relâchement du bouton, qui correspond à la sélection d'une nouvelle valeur.

Pour palier à cette redondance, la classe JList dispose d'une méthode getValueIsAdjusting() permettant de savoir si nous sommes ou non en phase de transition.

En réalité, cette précaution n'est pas toujours suffisante. Le mieux c'est de contrôler la présence de la chaîne de caractères sélectionnée.
.

Mise en oeuvre de deux boîtes de liste

A titre d'exemple, je vous propose de mettre au point une visionneuse de photos. Une boîte de liste, sur la gauche, permet de choisir le répertoire à consulter. Une autre boîte de liste, en dessous, affiche l'ensemble des photos que contient le répertoire sous forme de vignettes. Il suffit ensuite de cliquer sur la vignette pour afficher la photo en plus grand format.

Codage correspondant
import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Vector;
import javax.imageio.*;
import javax.swing.event.*;

public class Choix extends JFrame {   
   private String base = "C:/.";
   private Répertoires répertoires = new Répertoires();
   private ListePhotos photos = new ListePhotos();
   private Photo photo = new Photo();
   
   public Choix() {
      super("Visionneuse");
      add(new JScrollPane(répertoires), BorderLayout.WEST);
      add(new JScrollPane(photos), BorderLayout.SOUTH);
      add(photo);
      setSize(600, 500);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
   
   private class Répertoires extends JList implements ListSelectionListener { 
      private Vector<String> noms = new Vector<String>();
      private Icon icône = new ImageIcon("répertoire.gif");
      
      public Répertoires() {
         setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
         setCellRenderer(new Rendu());
         setFixedCellWidth(190);         
         addListSelectionListener(this);
         change();
      }
      
      public void change() {      
         setTitle(base);
         File[] fichiers = new File(base).listFiles();
         noms.clear();
         noms.add("..");
         for (File fichier : fichiers)
            if (fichier.isDirectory()) noms.add(fichier.getName());
         setListData(noms);   
      }

      public void valueChanged(ListSelectionEvent e) {
        String nom = (String)getSelectedValue();
         if (nom != null) {           
            if (!nom.equals("..")) {
               String nouveau = base + '/' + nom;
               base = nouveau;                   
            }
            else {
               String[] rep = base.split("/");
               if (rep.length>2) {
                  StringBuilder nouveau = new StringBuilder(rep[0]);
                  for (int i=1 ; i<rep.length-1; i++) nouveau.append( '/'+rep[i]);
                  base = nouveau.toString();
               }
            }
            change();   
            photos.change();
         }
      }
      
      private class Rendu extends JLabel implements ListCellRenderer { 
         public Rendu() {
            setIcon(new ImageIcon("répertoire.gif"));
            setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 3));
         }
         
         public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            setText((String)value);  
            return this;
         }         
      }
   }

   private class ListePhotos extends JList implements ListSelectionListener { 
      private Vector<String> noms = new Vector<String>();
      private final int largeur = 170;
      
      public ListePhotos() {
         setBackground(Color.BLACK);
         setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
         setCellRenderer(new Rendu());
         setFixedCellWidth(largeur+20);
         setFixedCellHeight(largeur*3/4+30);           
         setLayoutOrientation(JList.HORIZONTAL_WRAP);
         setVisibleRowCount(1);
         addListSelectionListener(this);
         change();
      }
      
      public void change() {      
         setTitle(base);
         File[] fichiers = new File(base).listFiles(new Filtre());
         noms.clear();
         for (File fichier : fichiers) {
            noms.add(fichier.getName());
         }
         setListData(noms);   
         setSelectedIndex(0);
         Choix.this.validate();
      }

      public void valueChanged(ListSelectionEvent e) {
        String nom = (String)getSelectedValue();
         if (nom != null) {           
            photo.change(new ImageIcon(base+'/'+nom).getImage());
         }
      }
      
      private class Filtre implements FilenameFilter {
         public boolean accept(File rep, String nom) {
            return nom.matches(".+jpg");
         }      
      }      
      
      private class Rendu extends JComponent implements ListCellRenderer { 
         private Image photo = null;
         
         public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 
            photo = new ImageIcon(base+'/'+(String)value).getImage();
            return this;
         }

         @Override
         protected void paintComponent(Graphics g) {
            if (photo != null) {
               int hauteur = (int)(largeur * photo.getHeight(null) / (double)photo.getWidth(null));
               g.drawImage(photo, 10, 3, largeur, hauteur, null);
            }
         }         
      }     
   }   
   
   private class Photo extends JComponent {
      private Image photo = null;
      
      public void change(Image photo) {
         this.photo = photo;
         repaint();
      }
      
      @Override
      protected void paintComponent(Graphics g) {
         if (photo != null) {
            int largeur = getWidth();
            int hauteur = (int)(largeur * photo.getHeight(null) / (double)photo.getWidth(null));
            g.drawImage(photo, 0, 0, largeur, hauteur, null);
         }
      }         
   }
   
   public static void main(String[] args) { new Choix(); }
}

 

Choix du chapitre Liste déroulante (liste combinée) - JComboBox

Dès que le nombre d'options augmente, les boutons radio ne conviennent plus, car ils occupent trop d'espace à l'écran. Vous pouvez, dans ce cas, utiliser une liste déroulante. Lorsque l'utilisateur clique sur le champ, une liste de choix se déroule ; il peut ainsi faire une sélection. La classe JComboBox (boîte combo) implémente cette fonctionnalité.

Sélection d'un élément par l'utilisateur

Une liste déroulante est à la croisée d'une zone de texte et d'une liste. Elle affiche une seule ligne de texte (éventuellement avec une image) et une flèche vers le bas, du côté droit. Lorque nous cliquons sur la flèche, la boîte combo s'ouvre pour afficher une liste d'éléments. Nous choisissons alors un élément en cliquant dessus. Dès que le choix est effectué, la boîte combo se referme ; la liste disparaît et le nouvel élément choisit s'affiche dans le champ de texte.

Au départ, c'est le premier choix de la liste qui est affiché dans la zone de texte.
.

Liste combinée

Si la liste déroulante est configurée pour accepter une saisie (éditable), il est alors possible de changer le choix actuel en tapant dans le champ, comme un champ de texte. La liste déroulante est aussi appelée liste combinée ; elle allie la souplesse d'un champ de texte et une liste de choix prédéfinis.

Il faut appeler la méthode setEditable() pour que la liste puisse être modifiable. Notez que la saisie ne modifie que l'élément courant et ne change pas le contenu de la liste.

Par défaut, le champ de texte associé à une boîte combo n'est pas éditable, ce qui signifie qu'il sert seulement à présenter la sélection courante de la liste. Mais, effectivement, il peut être rendu éditable. L'utilisateur peut alors y entrer, soit une valeur de la liste (en la sélectionnant), soit une valeur de son choix (en la saisissant classiquement, par le clavier ou par copier/coller).

On notera bien que, dans ce cas, la nouvelle valeur entrée n'est pas ajoutée automatiquement par Java dans la liste. On verra cependant que nous disposons de méthodes permettant de modifier dynamiquement la liste, donc d'effectuer éventuellement de tels ajouts si nous le désirons.

Création d'une liste déroulante

Nous créons une liste déroulante comme une liste classique à l'aide du constructeur adapté à la situation requise :

  1. JComboBox() : Préparer une liste déroulante vide qui sera complété plus tard à la suite d'événements spécifiques à l'aide d'un enchaînement de méthodes addItem().
  2. JComboBox(Object[] liste) : Construire la liste déroulante au moyen d'un tableau d'objet, comme par exemple un tableau de chaînes de caractères : String[]. Dans ce cas de figure, le nombre d'élément est prédéterminé.
  3. JComboBox(Vector<?> liste) : Nous pouvons aussi construire la liste déroulante au moyen d'un tableau dynamique représenté par la classe Vector<?>. Cette fois-ci, le nombre d'élément n'est pas spécialement fixé.
  4. JComboBox(ComboBoxModel modèleDeListe) : La classe JComboBox est conçue, comme les autres, suivant le modèle MVC. Ainsi, il est possible de construire une liste déroulante par rapport à une autre en connectant les modèles entre eux au moyen de la méthode getModel() de la première liste déjà construite.

Ajout, insertion ou suppression de nouvelles rubriques à la liste déroulante

A tout moment, il est possible d'ajouter, d'insérer ou de supprimer de nouvelles rubriques à votre liste déroulante. Voici l'ensemble des méthodes que vous avez à votre disposition pour réaliser cela :

  1. Ajouter une nouvelle rubrique à la fin de votre liste déroulante : addItem(Object rubrique).
  2. Insérer une nouvelle rubrique à un endroit spécifique : insertItemAt(Object rubrique, int position).
  3. Supprimer toutes les rubriques : removeAllItems().
  4. Supprimer une rubrique spécifique : removeItem(Object rubrique).
  5. Supprimer une rubrique suivant la position spécifiée : removeItemAt(int position).
  6. Connaître le nombre de rubriques déjà présentes : getItemCount();

Si vous devez rajouter un grand nombre d'éléments à une liste déroulante, la méthode addItem() fonctionnera mal. Construisez plutôt un DefaultComboBoxModel, remplissez-le en appelant addElement(), puis appelez la méthode setModel() de la classe JComboBox.

Imposer une sélection

Comme pour une boîte de liste, nous pouvons forcer la sélection d'une liste déroulante:

  1. d'un élément de rang donné par la méthode setSelectedIndex(int).
  2. d'une valeur particulière au travers de la méthode setSelectedItem(Object).

Accès aux rubriques sélectionnées ou pas

Bien entendu, si nous avons une liste de choix, c'est ultérieurement pour récupérer la valeur sélectionnée afin de réaliser le traitement souhaité. Au dela de ça, vous avez ici quelques méthodes intéressantes qui vous permettent de connaître quelles sont les rubriques actuelles de votre liste déroulante, et quel est, éventuellement, la nouvelle valeur saisie par l'utilisateur :

  1. getSelectedItem() fournit la valeur sélectionnée, qu'il s'agisse d'une valeur provenant de la liste prédéfinie ou d'une valeur saisie dans le champ de texte associé. On notera que son résultat est de type Object et non String. Il faudra donc procéder à une conversion explicite.
  2. getSelectedIndex() fournit aussi le rang de la valeur sélectionnée. Si cette information est généralement peu intéressante dans le cas d'un champ de texte non editable, elle le devient pour un champ de texte éditable. En effet, lorsque l'utilisateur entre effectivement une information, la méthode getSelectedIndex() fournit la valeur -1. Il est ainsi possible de discerner une saisie dans le champ de texte d'une sélection dans la liste, avec une exception toutefois lorsque l'utilisateur saisie une valeur figurant déjà dans la liste.

Apparence de la liste déroulante

Par défaut, une boîte de liste affiche toutes les options présentes dans la liste dans la mesure de la capacité du conteneur, ce qui la différencie de la boîte combo qui elle, n'affiche qu'une option à la fois (au repos). Bien entendu, nous pouvons modifier ce comportement initial pour que la visualisation de votre liste corresponde à l'apparence souhaitée.

  1. Barre de défilement : Contrairement à la boîte de liste, la boîte combo sera dotée d'un ascenceur dès que son nombre de valeur sera supérieur à 8. Nous pouvons modifier le nombre de valeurs visibles par la méthode setMaximumRowCount().
  2. Affichage des éléments de la liste : Comme pour la boîte de liste, la boîte combo est capable d'afficher directement des chaînes de caractères ou des icônes. Aussi, si vous devez afficher, pour chaque rubrique, autre chose que ces deux types d'éléments, il faudra lui expliquer comment faire. Cela se fait cette fois-ci au travers de la méthode setRenderer(). En argument de cette méthode, vous devez alors spécifier également un objet qui implémente l'interface ListCellRenderer. Attention, il faut impérativement s'occuper de l'affichage de l'ensemble de la liste, ce qui peut être fastidieux. Je rappelle que cette interface possède une seule méthode getListCellRendererComponent() que vous êtes donc obligé de redéfinir afin d'indiquer comment doit se faire la présentation souhaités. Voici la signature de cette méthode :

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)

Evénements générés par une liste déroulante

Comme pour une boîte de liste, nous pourrons parfois se contenter d'accéder à l'information sélectionnée sur une action externe :

  1. bouton associé,
  2. fermeture d'un boîte de dialogue contenant la liste déroulante.
Mais, plus souvent, nous chercherons à intercepter directement les événements générés par la liste déroulante. Or, contrairement à la boîte de liste, et de façon plus classique, la liste déroulante génère des événements de type Action que nous connaissons parfaitement bien :
  1. lors d'une sélection d'une valeur dans la liste,
  2. lors de la validation du champ de texte (lorsqu'il est éditable).
Par ailleurs, la boîte combo génère des événements de type Item (et non plus de type ListSelection comme la boîte de liste) à chaque modification de la sélection. Comme la catégorie ListSelection, la catégorie Item ne comporte qu'un seul type d'événement. Nous le traiterons par un écouteur approprié, c'est-à-dire une classe implémentant l'interface ItemListener qui comporte une seule méthode itemStateChanged().

Mais comme pour une boîte de liste à sélection simple, nous obtenons toujours deux événements (suppression d'une sélection, nouvelle sélection), qu'il s'agisse d'une saisie dans le champ texte ou d'une nouvelle sélection de liste. Il n'existe pas de méthode comparable à getValueIsAdjusting() pour éliminer cette redondance.

Mise en oeuvre de deux listes déroulantes

Je vous propose de reprendre l'exemple précédent. Cette fois-ci, nous plaçons deux listes déroulantes sur la partie gauche à la place des boîtes de liste. La première liste déroulante propose le nom des répertoires disponibles. La seconde propose une liste de vignettes sous forme d'icône.

Codage correspondant
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.event.*;

public class Choix extends JFrame {   
   private String base = "C:/.";
   private Répertoires répertoires = new Répertoires();
   private ListePhotos photos = new ListePhotos();
   private Photo photo = new Photo();
   
   public Choix() {
      super("Visionneuse");
      JPanel panneau1 = new JPanel();
      panneau1.setLayout(new BorderLayout());
      JPanel panneau2 = new JPanel();
      panneau2.setLayout(new BorderLayout());
      panneau1.add(répertoires, BorderLayout.NORTH);
      panneau2.add(photos, BorderLayout.NORTH);
      panneau1.add(panneau2);
      add(panneau1, BorderLayout.WEST);
      add(photo);
      setSize(600, 400);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
   
   private class Répertoires extends JComboBox implements ActionListener {      
      public Répertoires() {
         change();
         setMaximumRowCount(20);
         addActionListener(this);        
      }
      
      public void change() {      
         setTitle(base);
         File[] fichiers = new File(base).listFiles();
         removeAllItems();     
         for (File fichier : fichiers)
            if (fichier.isDirectory()) addItem(fichier.getName()); 
         if (getItemCount()==0) addItem("");
         addItem("..");         
      }

      @Override
      public void actionPerformed(ActionEvent e) {
        String nom = (String)getSelectedItem();          
         if (!nom.equals("..")) {
            String nouveau = base + '/' + nom;
            base = nouveau;                   
         }
         else {
            String[] rep = base.split("/");
            if (rep.length>2) {
               StringBuilder nouveau = new StringBuilder(rep[0]);
               for (int i=1 ; i<rep.length-1; i++) nouveau.append( '/'+rep[i]);
               base = nouveau.toString();
            }
         }
         change();   
         photos.change();
      }
   }

   private class ListePhotos extends JComboBox implements ActionListener { 
      private final int largeur = 170;
      private File[] fichiers;
      
      public ListePhotos() {
         addActionListener(this);
         change();
      }
      
      public void change() {      
         setTitle(base);
         fichiers = new File(base).listFiles(new Filtre());
         removeAllItems();
         for (File fichier : fichiers) addItem(retailler(base+'/'+fichier.getName()));
      }

      @Override
      public void actionPerformed(ActionEvent e) {       
         try {
            photo.change(new ImageIcon(base+'/'+fichiers[getSelectedIndex()].getName()).getImage());
         }
         catch (Exception ex) { photo.change(null); }
      }
      
      private ImageIcon retailler(String nom) {
         BufferedImage imageRetaillée = null;
         try {
            BufferedImage source = ImageIO.read(new File(nom));
            imageRetaillée = new BufferedImage(largeur, largeur*3/4, source.getType());
            double ratio = largeur / (double)source.getWidth();
            AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
            int interpolation = AffineTransformOp.TYPE_BICUBIC;
            AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
            retaillerImage.filter(source, imageRetaillée);
         } 
         catch (IOException ex) { setTitle("Photo non récupérée"); }
         return new ImageIcon(imageRetaillée);
      }
      
      private class Filtre implements FilenameFilter {
         public boolean accept(File rep, String nom) {
            return nom.matches(".+jpg");
         }      
      }        
   }   
   
   private class Photo extends JComponent {
      private Image photo = null;
      
      public void change(Image photo) {
         this.photo = photo;
         repaint();
      }
      
      @Override
      protected void paintComponent(Graphics g) {
         if (photo != null) {
            int largeur = getWidth();
            int hauteur = (int)(largeur * photo.getHeight(null) / (double)photo.getWidth(null));
            g.drawImage(photo, 0, 0, largeur, hauteur, null);
         }
      }         
   }
   
   public static void main(String[] args) { new Choix(); }
}

 

Choix du chapitre Le champ fléché - JSpinner

JList et JComboBox sont deux façons de permettre à l'utilisateur de faire un choix dans une liste de valeurs. JComboBox apporte plus de souplesse lorsqu'elle est éditable, mais en général ces deux composants comportent les mêmes limites, puisqu'ils présentent à l'utilisateur une liste de choix fixes. Swing apporte un nouveau composant, JSpinner, utile pour les longues listes ou les listes sans fin de valeurs comme des nombres ou des dates.

Un JSpinner est un champ de texte disposant de deux boutons. Lorsque l'utilisateur clique dessus, la valeur du champ de texte est augmentée ou diminuée. JSpinner est un cousin de JComboBox. Il affiche une valeur dans un champ, mais au lieu de fournir une liste descendante de choix, il offre ainsi à l'utilisateur une paire de flèches ascendante et descendante permettant de se déplacer dans une fourchette de valeur.

Comme une boîte combo, un JSpinner peut être rendu éditable, permettant à l'utilisateur de saisir une valeur valide directement dans le champ.
.

Les valeurs du champ fléché peuvent être des nombres, des dates, des valeurs d'une liste ou, plus généralement, une suite de valeurs dans laquelle il est possible de naviguer. La classe JSpinner définit des modèles de données standard pour les trois premiers cas. Vous pouvez également définir votre propre modèle de données et ainsi obtenir des suites arbitraires.

Swing fournit ainsi trois types de bouton fléché, représentés par les trois modèles de données (classes qui implémentent l'interface SpinnerModel) différents du composant JSpinner :

  1. SpinnerNumberModel : (modèle par défaut) affiche des valeurs numériques. Il peut être configuré avec des valeurs initiale, minimale et maximale. Par défaut, sans spécification particulière au niveau de la construction, un champ fléché gère un entier, initialisé à 0, que les boutons augmentent ou diminuent de 1. Dans ce cas de figure, il n'existe pas de limites inférieure ou supérieure.
  2. SpinnerListModel : se comporte comme une boîte combo, spécifiant un nombre fixe d'objets.
  3. SpinnerDateModel : Une des fonctionnalités les plus intéressantes de JSpinner est peut être ce modèle là qui permet à l'utilisateur de choisir des dates calendaires en utilisant l'incrément de temps spécifié.

Il existe une classe supplémentaire, AbstractSpinnerModel, qui implémente également l'interface SpinnerModel. Comme son nom l'indique, cette classe est abstraite. Elle peut s'avérer utile dans le cas où vous décidiez de construire votre propre modèle de champ fléché.

Création d'un champ fléché

Par défaut, sans spécification particulière au niveau de la construction, un champ fléché gère un entier, initialisé à 0, que les boutons augmentent ou diminuent de 1. Dans ce cas de figure, il n'existe pas de limites inférieure ou supérieure. Vous pouvez récupérer la valeur actuelle en appelant la méthode getValue(). Attention, cette méthode renvoie un Object (pour être compatible avec les autres modèles de données). Transtypez cette valeur de retour en un Integer et récupérez la valeur envoyée.

JSpinner saisie = new JSpinner();
...
int valeurARécupérer = (Integer) saisie.getValue();

Il est possible, à tout moment, de spécifier une valeur par programme à l'aide de la méthode setValue().
.

Il existe un deuxième constructeur où vous avez la possiblité de spécifier le modèle de votre choix. Le paramètre de ce constructeur attend un modèle implémentant l'interface SpinnerModel. Comme nous l'avons décrit plus haut, il existe déjà quatre modèles qui implémentent cette interface : SpinnerListModel, SpinnerNumberModel, SpinnerDateModel et AbstractSpinnerModel.

JSpinner(SpinnerModel modèle)

Le champ fléché pour gérer des valeurs numériques - modèle SpinnerNumberModel

Le constructeur par défaut d'un JSpinner prévoit de travailler avec des valeurs numériques entières, ce qui est déjà intéressant pour pas mal de situation. Toutefois, dans de nombreux autres cas, il serait plus judicieux de prévoir une configuration plus adaptée à la situation. Nous pouvons, par exemple, modifier l'incrémentation pour qu'elle soit différente de 1 et fournir des limites inférieures ou supérieures. Vous devez alors passer par le modèle qui représente les valeurs numériques soit : SpinnerNumberModel.

SpinnerNumberModel modèle = new SpinnerNumberModel(10, Integer.MIN_VALUE, Integer.MAX_VALUE, 2);
JSpinner
saisie = new JSpinner(modèle);
...
int valeurARécupérer = modèle.getNumber().intValue();

Ici, nous avons construit un champ fléché dont la valeur initiale est 10 permettant à l'utilisateur de saisir une valeur entière quelconque (sans borne) avec un pas de 2.

Remarquez bien ici la présence de la méthode getNumber() de la classe SpinnerNumberModel qui renvoie la valeur actuelle. Cette méthode renvoie une valeur de type Number, qui rappelons-le représente toute les valeurs numériques. Revoir le cours sur les classes enveloppes.

Cette classe SpinnerNumberModel propose en réalité plusieurs constructeurs qui vont correspondre aux différents cas d'utilisation :

  1. SpinnerNumberModel() : Modèle numérique qui représente les valeurs entières sans valeurs limites avec 0 comme valeur initiale et 1 comme incrément. C'est ce modèle qui est proposé par défaut au composant JSpinner.
  2. SpinnerNumberModel(int valeur, int minimum, int maximum, int incrément) : Modèle numérique qui représente également les valeurs entières en spécifiant cette fois-ci des valeurs initiale, minimale et maximale, ainsi que la valeur de l'incrément. Remarquez bien que tous les paramètres de ce constructeur sont de type int. C'est ce constructeur que nous avons utilisé dans l'exemple précédent.
  3. SpinnerNumberModel(double valeur, double minimum, double maximum, double incrément) : Ce constructeur représente le modèle numérique pour les valeurs réelles et présente les mêmes fonctionnalités que le constructeur précédent, mais ici tous les types sont des double.
  4. SpinnerNumberModel(Number valeur, Comparable minimum, Comparable maximum, Number incrément) : Avec ce constructeur, vous avez la possibilité de choisir le type numérique qui vous intéresse, comme les long, les bytes, etc.

Le champ fléché pour gérer des objets quelconques - modèle SpinnerListModel

Les boutons fléchés ne sont pas limités à des valeurs numériques. Vous pouvez amener un bouton fléché à faire défiler toute suite de valeurs quelconques. Transférez simplement un SpinnerListModel au constructeur JSpinner. Le SpinnerListModel se comporte ainsi comme une boîte combo, spécifiant un nombre fixe d'objet.

Pour construire un SpinnerListModel, utilisez un tableau ou une classe implémentant l'interface List (comme un ArrayList) :

String options = new String[] {"petit", "moyen", "grand", "immense"};
SpinnerListModel
modèle = new SpinnerListModel(option);
JSpinner
saisie = new JSpinner(modèle);
...
String valeurARécupérer = (String) saisie.getValue();
// ou alors
String valeurARécupérer = (String) modèle.getValue();

  1. SpinnerListModel(Object[] liste) : Construit un modèle de liste à partir d'un tableau d'objets statique.
  2. SpinnerListModel(List<?> liste) : Construit un modèle de liste à partir d'une liste ou d'un tableau dynamique.

Il est possible, après coup, de proposer une nouvelle liste au moyen de la méthode setList(List<?>).
.

Le champ fléché pour gérer des dates - modèle SpinnerDateModel

Une des fonctionnalités les plus intéressantes de JSpinner est peut être le modèle SpinnerDateModel qui permet à l'utilisateur de choisir des dates calendaires en utilisant l'incrément de temps spécifié. SpinnerDateModel accepte un intervalle comme SpinnerNumberModel, mais les valeurs sont des objets Date, et l'incrément un champ constant java.utili.Calendar comme Calendar.DAY, Calendar.WEEK, et ainsi de suite.

Cette classe SpinnerDateModel propose en réalité un seul constructeur intéressant :

SpinnerDateModel(Date valeurInitiale, Comparable limiteInférieure, Comparable limiteSupérieure, int incrément)

L'exemple qui suit crée un Spinner affichant la date et l'heure courantes. Il permet à l'utilisateur de modifier la date en changeant de semaine, sur une période d'un an (plus ou moins six mois).

Calendar maintenant = Calendar.getInstance();
Calendar début = (Calendar) maintenant.clone();
début.add(Calendar.MONTH, -6);
Calendar fin = (Calendar) maintenant.clone();
fin.add(Calendar.MONTH, 6);
SpinnerDateModel modèle = new SpinnerDateModel(maintenant.getTime(), début.getTime(), fin.getTime(), Calendar.WEEK_OF_YEAR);
JSpinner choix = new JSpinner(modèle);    

Evénements générés par les boutons fléchés

Ce composant ne génère pas d'événement de type Action. Dans certains cas, nous pourrons nous contenter d'accéder à l'information sélectionnée sur une action externe au bouton fléché, par exemple :

  1. à la fermeture d'une boîte de dialogue contenant la boîte.
  2. sur l'action d'un bouton associé à ce champ fléché, servant ainsi à valider la sélection.

Dans certain cas, il peut être intéressant de proposer un traitement particulier lorsque l'utilisateur clique sur l'une des flèches. Dans ce cas là, un événement de type Change est généré. L'écouteur associé est l'interface ChangeListener qui possède une seule méthode stateChanged(ChangeEvent). Par ailleurs, lorsque l'utilisateur propose une nouvelle valeur sur le champ fléché à l'aide du clavier et valide sa saisie à l'aide de la touche <Entrée>, l'événement de type Change est également sollicité ce qui permet finalement de se passer de l'événement de type Action.

Proposer une édition personnalisée

JSpinner est une classe vraiment intéressante qui, dans le cas des dates, permet d'avoir une incrémentation relativement sophistiquée. Toutefois, de base, la présentation est imposée. Ainsi, en reprenant l'exemple de la sélection de dates, le composant affiche systématiquement la date et l'heure. L'idéal serait de pouvoir choisir sa propre visualisation : dans un cas afficher uniquement la date et dans une autre situation afficher l'heure suivant des formats personnalisés.

Nous connaissons déjà un composant qui réalise des saisies avec un format personnalisé pour les nombres et les dates. Souvenez-vous, il s'agit de la classe JFormattedTextField. L'idéal pour JSpinner serait d'avoir le principe du composant JFormattedTextField avec, en plus, le système de flèches pour incrémenter et décrémenter la valeur avec le pas souhaité.

Heureusement, tout a été prévu. Il existe effectivement des classes internes à la classe JSpinner qui s'occupe du formatage de l'information pour arriver à une édition personnalisée. Nous avons ainsi trois classes d'édition adaptées au modèle de représentation :
  1. JSpinner.NumberEditor : éditeur personnalisé correspondant au modèle JSpinnerNumberModel.
  2. JSpinner.ListEditor : éditeur personnalisé correspondant au modèle JSpinnerListModel.
  3. JSpinner.DateEditor : éditeur personnalisé correspondant au modèle JSpinnerDateModel.

Les deux éditeurs qui me semblent intéressants, est d'une part NumberEditor qui permet de travailler éventuellement avec des valeurs monétaires, et d'autre part DateEditor qui permet de personnaliser la présentation de la date qui, il faut bien le dire, peut s'afficher de bien des façons différentes.

Pour personnaliser son édition, il faut respecter la séquence suivante :
  1. Dans un premier temps, il faut tout simplement créer de façon classique le bouton fléché à votre convenance, suivant les contraintes souhaitées.
  2. Il faut ensuite créer un objet relatif à l'éditeur personnalisé. Pour cela, chaque classe interne dispose d'un constructeur avec deux paramètres. Le premier spécifie le champ fléché qui va utiliser ce formatage personnalisé. Sur le deuxième paramètre, vous placez le motif sous forme de chaîne de caractères qui sera alors interprété pour donner l'apparence désirée.
  3. Il suffit enfin de faire appel à la méthode setEditor() du JSpinner pour introduire ce nouvel éditeur personnalisé.
JSpinner.NumberEditor

A titre d'exemple, je vous propose de reprendre l'application qui permet de convertir des €uros à des francs. La zone de saisie correspondant à l'€uro est un champ fléché dont l'incrément est de 0,16 € :


import javax.swing.*;
import java.awt.*;
import java.text.DecimalFormat;
import javax.swing.event.*;

public class Choix extends JFrame implements ChangeListener {   
   private JSpinner €uro = new JSpinner(new SpinnerNumberModel(0.0, 0.0, Double.MAX_VALUE, 0.16));
   private JFormattedTextField franc = new JFormattedTextField(new DecimalFormat("#,##0.00 F"));
   
   public Choix() {
      super("Conversion");
      €uro.setEditor(new JSpinner.NumberEditor(€uro, "#,##0.00 €"));
      €uro.addChangeListener(this);
      add(€uro);
      franc.setHorizontalAlignment(JFormattedTextField.RIGHT);
      franc.setForeground(Color.RED);
      franc.setEditable(false);
      franc.setValue(0);
      add(franc, BorderLayout.SOUTH);
      setSize(200, 80);
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
     
   public static void main(String[] args) { new Choix(); }

   public void stateChanged(ChangeEvent e) {
      franc.setValue((Double)€uro.getValue()*6.55957);        
   }
}

Pour en savoir plus sur les motifs concernant le formatage des valeurs numériques, revoir le cours correspondant.
.

JSpinner.DateEditor

Le deuxième exemple que je vous propose est tout simplement d'afficher la date du jour est de pouvoir ensuite incrémenter cette date soit jour après jour (par défaut), soit mois après mois, soit année après année, à l'aide d'un champ fléché. Pour cela, il suffit de sélectionner l'élément que vous désirez incrémenter (ou décrémenter) à l'aide de la souris, par exemple le mois, et ensuite de cliquer sur la flèche souhaitée.


import javax.swing.*;
import java.awt.*;

public class Choix extends JFrame {   
   private JSpinner choix = new JSpinner(new SpinnerDateModel());
   
   public Choix() {
      super("Date...");
      choix.setEditor(new JSpinner.DateEditor(choix, "EEEE, dd MMMM yyyy"));
      add(choix);
      setSize(200, 60);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
      
   public static void main(String[] args) { new Choix(); }
}

Pour en savoir plus sur les motifs concernant le formatage des dates, revoir le cours correspondant.
.

Création de son propre modèle de champ fléché à l'aide de la classe abstraite AbstractSpinnerModel

Il est tout à fait possible de créer son propre modèle. Par exemple, nous pouvons envisager de proposer un champ fléché qui donne la suite des nombres premiers. Pour cela vous devez mettre en oeuvre une classe qui hérite de la classe abstraite AbstractSpinnerModel et redéfinir les quatre méthodes suivantes :

  1. Object getValue() : renvoie la valeur stockée dans le modèle.
  2. void setValue(Object valeur) : définit une nouvelle valeur. Elle lancera une exception IllegalArgumentException si la nouvelle valeur ne convient pas.
  3. Object getNextValue() : renvoie la valeur qui doit venir après ou null si la fin de l'itération est atteinte.
  4. Object getPreviousValue() : renvoie la valeur qui doit venir avant ou null si la fin de l'itération est atteinte.

Attention : La méthode setValue() doit appeler la méthode fireStateChanged() après avoir défini la nouvelle valeur, faute de quoi le champ fléché ne sera pas mis à jour.

Attention : Les méthodes geNextValue() et getPreviousValue() ne doivent pas modifier la valeur courante. Cliquer sur la flèche ascendante du bouton appelle la méthode getNextValue(). Si la valeur de retour n'est pas null, elle est définie par un appel à setValue(). C'est à l'aide de ces deux méthodes que nous spécifions la progression à suivre.


La suite des nombres premiers

import javax.swing.*;
import java.awt.*;

public class Choix extends JFrame {   
   private JSpinner choix = new JSpinner(new Modèle());
   
   public Choix() {
      super("Premiers...");
      add(choix);
      setSize(200, 60);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
      
   public static void main(String[] args) { new Choix(); }
   
   private class Modèle extends AbstractSpinnerModel {
      private int nombre = 1; 
      
      public Object getValue() {
         return nombre;
      }

      public void setValue(Object valeur) {
         if (!(valeur instanceof Integer)) throw new IllegalArgumentException();
         nombre = (Integer)valeur;
         fireStateChanged();
      }

      public Object getNextValue() {
         int n = nombre;
         while (!isPremier(++n));
         return n;
      }

      public Object getPreviousValue() {
         if (nombre==1) return 1;
         int n = nombre;
         while (!isPremier(--n));
         return n;
      }   
      
      private boolean isPremier(int nombre)
      {
          for (int i = nombre-1; i >= Math.sqrt(nombre); i--) {
             int resultat = nombre/i;
             if (resultat*i == nombre) return false;
          }
          return true;
      }
   }
}

 

Choix du chapitre Les glissières (curseurs) et les barres de défilement

Les zones déroulantes permettent à l'utilisateur de choisir parmi un ensemble de valeurs. Les glissières, appelées également curseurs, et les barres de défilement proposent un choix exhaustif de valeurs (numériques entières exclusivement), par exemple, n'importe quel nombre compris entre 1 et 100. La glissière est représentée par la classe JSlider alors que la barre de défilement est représentée par la classe JScrollBar.

Nous connaissons bien les barres de défilement, je n'insisterais donc pas sur leurs aspects visuels. La glissière, quant à elle, peut comporter des repères, petites lignes tracées à certaines distances le long de la zone de défilement. Les repères majeurs sont légèrement plus épais que les mineurs. Les barres de défilement, ainsi que les glissières peuvent être représentées suivant deux directions possibles, horizontalement ou verticalement.

Par défaut, une glissière présente son curseur au centre de la glissière, alors que par défaut, la barre de défilement place son curseur d'un côté de la barre, sur la position la plus basse.

Création d'une glissière ou d'une barre de défilement

Au moment de la création des barres de défilements et des glissières, vous avez, suivant le cas, la possiblité de proposer une orientation. Vous pouvez également préciser leurs valeurs minimum, maximum, ainsi qu'une valeur intiale. La barre de défilement gère un autre paramètre, la largeur du curseur au départ.

  1. JSlider() : Création d'une glissière horizontale, avec comme valeur minimale 0, comme valeur maximale 100 et valeur initiale 50.
  2. JSlider(int orientation) : Création de la même glissière que ci-dessus, mais avec le choix de l'orientation : SwingConstants.VERTICAL ou SwingConstants.HORIZONTAL.
  3. JSlider(int minimum, int maximum) : Création d'une glissière horizontale avec réglage de la valeur minimale et maximale. La valeur initiale se situe automatiquement à la moyenne des deux valeurs proposées.
  4. JSlider(int minimum, int maximum, int initial) : Création d'une glissière horizontale avec spécification de la valeur minimale, maximale et initiale.
  5. JSlider(int orientation, int minimum, int maximum, int initial) : Création d'une glissière en spécifiant son orientation ainsi que ses valeurs minimale, maximale et initiale.
  6. JScrollBar() : Création d'une barre de défilement verticale, avec comme valeur minimale 0, comme valeur maximale 100, comme valeur initiale 0. La largeur du curseur par défaut est de 10 unités. ATTENTION, la valeur maximale que peut délivrer une barre de défilement dépend de la largeur du curseur. Ainsi, par défaut, la valeur donnée par la méthode getValue() est de 90 (100-10).
  7. JScrollBar(int orientation) : Création de la même barre de défilement ci-dessus, mais avec le choix de l'orientation : SwingConstants.VERTICAL ou SwingConstants.HORIZONTAL.
  8. JScrollBar(int orientation, int valeur, int largeurCurseur , int minimum, int maximum) : Création d'une barre de défilement en spécifiant son orientation, sa valeur de départ, sa largeur du curseur, sa valeur minimale et sa valeur maximale.

Récupérer ou suggérer de nouvelles valeurs

Au fur et à mesure que l'utilisateur fait glisser le curseur, la valeur évolue entre les valeurs minimale et maximale, soit de la barre de défilement, soit de la glissière.

Evénements générés par les barres de défilement

Il est très facile de répondre à des événements provenant de ces deux composants. Chacun comporte un type d'événement spécifique :

  1. Pour la barre de défilement : l'événement envoyé est de type AdjustmentEvent. Il faut alors prévoir un écouteur qui implémente l'interface AdjustmentListener qui possède la méthode adjustmentValueChanged(AdjustmentEvent).
  2. Pour la glissière : l'événement envoyé est cette fois-ci un ChangeEvent. Nous connaissons déjà ce type d'événement. Je rappelle que vous devez alors implémenter l'interface ChangeListener qui possède la méthode stateChanged(ChangeEvent).

Apparence des glissières

L'apparence des glissières est largement configurable. Voici ci-dessous tous les réglages que vous pouvez proposer afin d'obtenir plus ou moins d'informations ergonomiques. Ces réglages sont effectués à partir de méthodes bien spécifiques :


Glissière par défaut

Lorsque nous utilisons l'un des constructeurs que nous venons de découvrir, sans faire appel à d'autres méthodes, voici l'apparence qui vous est proposée par défaut :

public class Choix extends JFrame implements ChangeListener { 
   private JSlider glissière = new JSlider();
   private JFormattedTextField valeur = new JFormattedTextField(50);
   
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      add(glissière);
      valeur.setEditable(false);
      valeur.setForeground(Color.RED);
      valeur.setColumns(2);
      add(valeur);
      setSize(250, 70);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
      
   public static void main(String[] args) { new Choix(); }

   public void stateChanged(ChangeEvent e) {
      valeur.setValue(glissière.getValue());
   }
}
Les repères

Vous pouvez améliorer le curseur en affichant les repères que nous avons déjà évoqués plus haut. Nous pouvons proposer soit les repères majeurs, soit les repères mineurs ou bien les deux. Cela se fait, respectivement, à l'aide des méthodes setMajorTickSpacing() et setMinorTickSpacing(). Ces deux méthodes ne font que définir les unités pour les repères. Pour les afficher, vous devez également appeler la méthode setPaintTicks(true). Voici ci-dessous comment proposer des grands repères toutes les 10 unités et des petits toutes les 5 unités :

...
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(10);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);
      add(glissière);     
...
}

Les petites et les grandes marques de repères sont indépendantes. Vous pouvez parfaitement définir les grands marques toutes les 20 unités et les petits toutes les 7 unités, mais vous risquez d'obtenir une échelle assez déroutante. l'idéal est tout de même que l'unité choisie pour la grande marque soit multiple de la valeur spécifiée pour la petite marque.

S'aligner sur les repères

Vous pouvez forcer le curseur à s'aligner sur les repères. Chaque fois que l'utilisateur relâche le curseur dans ce mode de déplacement, il est positionné automatiquement sur le repère le plus proche. Vous activez ce mode de fonctionnement à l'aide de la méthode setSnapToTicks(true). Avec ce mode, en reprenant l'exemple précédent, nous ne pouvons obtenir que des valeurs multiple de 5 :

...  
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(10);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);
      glissière.setSnapToTicks(true);
      add(glissière);
...
}
Affichage des valeurs pour les repères importants

Vous pouvez demander l'affichage de valeurs de repères pour les grandes marques à l'aide de la méthode setPaintLabels(true). Ainsi, en reprenant l'exemple précédent, et en modifiant l'espace des grandes marques à 20, voici ce que nous pouvons obtenir :

...
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(20);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);
      glissière.setSnapToTicks(true);
      glissière.setPaintLabels(true);
      add(glissière);
...  
Proposer du texte ou des icônes à la place des valeurs numériques affichées

Vous pouvez aussi fournir d'autres libellés, comme des chaînes ou des icônes. Le processus est un peu plus compliqué. Vous devez remplir une table de hachage avec des clés de type Integer, qui permet de choisir les repères, et des valeurs de type Component. Vous appelez ensuite la méthode setLabelTable(). Ainsi, les composants spécifiés sont placés sous les marques de repère choisies.

...
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(20);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);glissière.setSnapToTicks(true);
      Hashtable<Integer, JLabel> étiquettes = new Hashtable<Integer, JLabel>();
      étiquettes.put(0, new JLabel("foncé"));
      étiquettes.put(50, new JLabel("neutre"));
      étiquettes.put(100, new JLabel("clair"));
      glissière.setPaintLabels(true);
      glissière.setLabelTable(étiquettes);
      add(glissière);
...  

Si les marques ou libellés de repères ne s'affichent pas, vérifiez que vous avez bien appelé setPaintTicks(true) et setPaintLabels(true).
.

Suppression du couloir dans lequel se déplace le curseur

Il est possible de réaliser un affichage plus sobre en proposant de ne plus présenter le guidage de la glissière au moyen de la méthode setPaintTrack(false) :

...public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(20);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);
      glissière.setSnapToTicks(true);
      glissière.setPaintLabels(true);
      glissière.setPaintTrack(false);
      add(glissière);
...
Inversion du sens de la glissière

Enfin, il est tout à fait possible de faire en sorte que la valeur minimale se trouve plutôt à droite et la valeur maximale vers la gauche, et de proposer ainsi une glissière dont le sens croissant va de la droite vers la gauche, ceci au moyen de la méthode setInverted(true) :

...
   public Choix() {
      super("Glissière");
      getContentPane().setBackground(Color.YELLOW);
      setLayout(new FlowLayout());
      glissière.setOpaque(false);      
      glissière.addChangeListener(this);
      glissière.setMajorTickSpacing(20);
      glissière.setMinorTickSpacing(5);
      glissière.setPaintTicks(true);
      glissière.setSnapToTicks(true);
      glissière.setPaintLabels(true);
      glissière.setInverted(true);
      add(glissière);
...

Apparence des barres de défilement

Par défaut, une barre de défilement est verticale avec une valeur minimale à 0 une valeur maximale de lecture de 90 (100 moins l'épaisseur du curseur qui est de 10 unités par défaut). Cette fois-ci, comme tout ascenceur, l'utilisateur peut aussi bien agir sur le curseur central que sur les flèches extrêmes. Le pas d'évolution, incrémentation ou décrémentation, est de 1 par défaut. Il est tout à fait possible de choisir un incrément plus grand au moyen de la méthode setUnitIncrement().

Vous pouvez, bien entendu, proposer une barre de défilement totalement personnalisée, en spécifiant respectivement : son orientation, sa valeur initiale, la largeur du curseur central, sa valeur la plus basse possible et la valeur maximale que le curseur central peut atteindre sur sa partie droite.


Création d'une barre de défilement personnalisée avec un pas de 5

A titre d'exemple, je vous propose de fabriquer une barre de défilement horizontale avec un curseur d'une largeur de 30 qui peut évoluer de 0 à 100. Vérifiez bien que la valeur maximale lisible est de 70. Par ailleurs, la barre ne propose que les valeurs multiples de 5.


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

public class Choix extends JFrame implements AdjustmentListener { 
   private JScrollBar barre = new JScrollBar(SwingConstants.HORIZONTAL, 50, 30, 0, 100);
   private JFormattedTextField valeur = new JFormattedTextField(50);
   
   public Choix() {
      super("Barre de défilement");
      getContentPane().setBackground(Color.YELLOW);
      barre.setOpaque(false);      
      barre.addAdjustmentListener(this);
      barre.setUnitIncrement(5);
      add(barre);
      valeur.setEditable(false);
      valeur.setForeground(Color.RED);
      valeur.setColumns(2);
      add(valeur, BorderLayout.EAST);
      setSize(250, 55);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
      
   public static void main(String[] args) { new Choix(); }

   public void adjustmentValueChanged(AdjustmentEvent e) {
      valeur.setValue(barre.getValue());
   }
}

Mise en oeuvre d'une glissière

Pour terminer sur ce sujet, je vous propose de mettre en place une petite application qui permet d'afficher une photo avec la possibilité de régler le zoom au travers d'une glissière. Le réglage permis pour le zoom va de 30% à 100%.


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

public class Choix extends JFrame { 
   private Photo photo = new Photo();
   
   public Choix() {
      super("Zoom sur une photo");
      add(photo);
      setSize(350, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }      
      
   public static void main(String[] args) { new Choix(); }
   
   private class Photo extends JPanel {
      private final Image photo = new ImageIcon("Rouge-gorge.jpg").getImage();
      private final int largeur = photo.getWidth(null);
      private final int hauteur = photo.getHeight(null);
      private int zoom = 30;
      private Color transparence = new Color(0F, 0F, 0.5F, 0.1F);
      
      public Photo() {
         setLayout(new BorderLayout());
         setBackground(Color.BLACK);
         add(new Zoom(), BorderLayout.EAST);
      }
      
      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D s = (Graphics2D) g;
         s.drawImage(photo, -(largeur*zoom/100-getWidth())/2, 
                                            -(hauteur*zoom/100-getHeight())/2, 
                                               largeur*zoom/100, hauteur*zoom/100, null);
         s.setPaint(transparence);
         s.fillRect(getWidth()-60, 0, getWidth(), getHeight());
      }     
      
      private class Zoom extends JSlider implements ChangeListener {
         public Zoom() {
            super(SwingConstants.VERTICAL, zoom, 100, zoom);
            setOpaque(false);      
            setForeground(Color.WHITE);
            addChangeListener(this);
            setMajorTickSpacing(10);
            setMinorTickSpacing(2);
            setPaintTicks(true);
            setPaintLabels(true);      
         }

         public void stateChanged(ChangeEvent e) {
            zoom = getValue();
            Photo.this.repaint();
         }      
      }
   }      
}