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.
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.
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 :
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.
Les boutons Swing génèrent trois types d'événements :
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 :
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.
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.
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.
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 :
Vous avez ci-dessous les différents états du bouton de conversion :
Vous remarquez la présence d'un raccourci clavier sur le caractère 'C' du bouton. Le raccourci clavier correspondant est donc <Alt+C>.
.
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(); } }
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")); ... } ... }
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.
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.
.
Nous aurons souvent besoin d'exploiter un bouton à bascule de deux façons différentes :
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 :
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(); } }
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.
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); } }
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.
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.
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.
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é.
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.
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.
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(); } }
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.
Cette fois-ci, les libellés n'existent plus (informations redondantes). Deux icônes seulement représentent les deux cases à cocher.
... 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(); } }
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.
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.
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.
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(); } }
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.
La sélection d'un élément ou d'un ensemble d'éléments est classique dans un environnement graphique :
Nous pouvons créer des boîtes de liste à l'aide du constructeur adapté à la situation requise :
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 :
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.
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.
Initialement, aucune valeur n'est sélectionnée dans la liste. Le cas échéant, nous pouvons forcer la sélection :
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é :
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 :
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 :
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.
.
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.
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(); } }
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é.
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.
.
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.
Nous créons une liste déroulante comme une liste classique à l'aide du constructeur adapté à la situation requise :
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 :
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.
Comme pour une boîte de liste, nous pouvons forcer la sélection d'une liste déroulante:
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 :
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.
Comme pour une boîte de liste, nous pourrons parfois se contenter d'accéder à l'information sélectionnée sur une action externe :
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.
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.
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(); } }
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 :
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é.
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().
.
JSpinner(SpinnerModel modèle)
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 :
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.
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();
Il est possible, après coup, de proposer une nouvelle liste au moyen de la méthode setList(List<?>).
.
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.
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);
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 :
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.
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é.
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.
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.
.
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.
.
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 :
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.
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; } } }
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.
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.
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.
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 :
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 :
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()); } }
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.
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); ... }
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); ...
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).
.
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); ...
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); ...
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.
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()); } }
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(); } } } }