Dans cette partie nous allons nous concentrer sur l'étude de tout ce qui représente une saisie de texte ou de valeurs formatée comme les valeurs numériques ou les dates, jusqu'à la mise en place d'éditeurs relativement sophistiqués.
Nous entendons par saisie, toutes les valeurs introduites à l'aide du clavier. Il existe d'autres possibilités qui permettent de récupérer des valeur numériques notamment à partir de composants graphiques comme les curseurs ou les slidders. Ce sera l'objet d'une autre étude.

Le composant le plus complexe de Swing est le composant JTextComponent, qui est un éditeur puissant. Il fait partie du paquetage javax.swing.text. Vous ne pouvez pas construire vous-même un objet JTextComponent, car il s'agit d'une classe abstraite. En réalité, vous employez plus souvent l'une des sous-classe suivantes :
La possibilité d'afficher si facilement du texte formaté est une fonctionnalité extrêmement puissante. Par exemple, l'affichage de document HTML dans une application simplifie l'ajout d'une aide en ligne basée sur une version HTML du manuel de l'utilisateur. De plus, le texte formaté procure à une application, un moyen professionnel d'afficher sa sortie à un utilisateur.
Pour étudier les modifications apportées à des composants texte, il est nécessaire de connaître la façon dont ils implémentent l'architecture MVC (Modèle-Vue-Contrôleur). Les composants texte vont nous permettre de bien distinguer les partie M et VC. Le modèle de composants textes est un objet baptisé Document.
Document est une interface. Cette interface est implémentée par la classe abstraite AbstractDocument. Cette classe abstraite est héritée par la classe fille PlainDocument. Ainsi, lorsque nous faisons référence un élément de type Document, il s'agira en interne, d'un objet PlainDocument.
Lorsque nous ajoutons ou supprimons du texte d'un JTextField ou d'un JTextArea, le Document correspondant est modifié. C'est le composant lui-même, et non les composants visuels, qui génère les événements texte lorsqu'un changement se produit. Par conséquent, pour être informé des modification de JTextArea, nous nous enregistrons auprès du Document concerné, et non auprès du composant JTextArea proprement dit :
JTextArea saisie = new JTextArea(); Document texte = saisie.getDocument();
texte.addDocumentListener(écouteur);
En outre, les composants JTextField génèrent un ActionEvent à chaque fois que l'utilisateur appuie sur la touche Entrée dans le champ. Pour recevoir ces événements, implémentez l'interface ActionListener et appelez addActionListener() pour effectuer l'enregistrement de votre écouteur.
JTextComponent délivrent un certain nombre de fonctionnalités communes à toutes ces classes filles. En voici quelques unes qui me paraissent intéressantes (liste non exhaustive) :
saisie.setMargin(new Insets(haut, gauche, bas, droit));
JTextField permet à l'utilisateur d'entrer et d'éditer une ligne unique de texte simple. Nous pouvons lire et écrire le texte avec les méthode getText() et setText() héritées de la super-classe JTextComponent. Nous pouvons également sollliciter les méthodes propres à la classe JTextField :
JTextField déclenche un ActionEvent aux écouteurs de type ActionListener quand l'utilisateur tape sur la touche "Entrée" du clavier. Nous pouvons éventuellement spécifier le texte de la commande d'action envoyée avec la'événement ActionEvent en appelant la méthode setActionCommand().
Plusieurs constructeurs sont à votre disposition pour créer des champs de texte. Le premier est le constructeur par défaut. La largeur du champ de texte dépend alors du gestionnaire utilisé. Dans la plupart des cas, vous devrez toutefois spécifier ultérieurement le nombre de colonne attendu au moyen de la méthode setColumns().
JTextField saisie = new JTextField("Introduisez votre texte", 20);Ce code crée un champ de texte et l'initialise avec la chaîne Introduisez votre texte. Le second paramètre en définit la largeur. Dans notre exemple, la largeur est de 20 colonnes.
Malheureusement, une colonne est une unité de mesure assez imprécise. Elle représente la largeur attendue d'un caractère dans la police employée pour le texte. Si vous prévoyez des entrées utilisateurs d'au plus n caractères, vous êtes supposé indiquer n en tant que largeur de colonne. Dans la pratique, cette mesure ne donne pas de très bons résultats et vous devrez ajouter 1 ou 2 à la longueur d'entrée maximale prévue.
Gardez aussi à l'esprit que le nombre de colonnes n'est qu'une suggestion pour AWT pour indiquer une taille de préférence. Si le gestionnaire de mise en forme a besoin d'agrandir ou de réduire le champ de texte, il peut ajuster la taille.
La largeur de colonne que vous définissez dans le constructeur de la classe JTextField ne limite pas pour autant le nombre de caractères que l'utilisateur peut taper. Il peut introduire des chaînes plus longues ; toutefois, la vue de l'entrée défile lorsque le texte dépasse la longueur du champ souhaitée.
JTextField saisie = new JTextField(20);
Les méthodes suivantes permettent de travailler avec le texte de la zone de saisie :
String texte = saisie.getText().trim();
Les touches de raccourci (Ctrl-C, Ctrl-V ou Ctrl-X), qui permettent de faire du copier-coller en passant par le presse papier, sont tout à fait opérationnels avec ce composant. Cette fonctionnalité est en réalité héritée de la super-classe JTextComponent.
Vous avez ci-dessous un exemple de codage qui permet de comprendre l'utilisation de ces champs de texte avec les diverses méthodes et phases de construction que nous venons de voir. J'en profite pour prendre quelques fonctionnalités de la super-classe JTextComponent :

package saisie; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Champ extends JFrame implements ActionListener { private JLabel intituléNom = new JLabel("Nom :"); private Saisie nom = new Saisie("Votre nom"); private JLabel intituléPrénom = new JLabel("Prénom :"); private Saisie prénom = new Saisie("Votre prénom"); private JButton validation = new JButton("Valider"); private Saisie résultat = new Saisie("Effectuer votre saisie"); public Champ() { super("Saisie des références"); résultat.setEditable(false); gestionDisposition(); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); setVisible(true); validation.addActionListener(this); } private class Saisie extends JTextField { public Saisie(String texte) { super(texte, 20); setFont(new Font("Verdana", Font.BOLD, 12)); setMargin(new Insets(0, 3, 0, 0)); } } private void gestionDisposition() { GroupLayout groupe = new GroupLayout(getContentPane()); getContentPane().setLayout(groupe); groupe.setAutoCreateContainerGaps(true); groupe.setAutoCreateGaps(true); GroupLayout.ParallelGroup horzGroupe = groupe.createParallelGroup(); GroupLayout.SequentialGroup vertGroupe = groupe.createSequentialGroup(); horzGroupe.addComponent(intituléNom).addComponent(nom).addComponent(intituléPrénom).addComponent(prénom); horzGroupe.addComponent(validation).addComponent(résultat); vertGroupe.addComponent(intituléNom).addComponent(nom).addComponent(intituléPrénom).addComponent(prénom); vertGroupe.addComponent(validation).addComponent(résultat); groupe.setHorizontalGroup(horzGroupe); groupe.setVerticalGroup(vertGroupe); } public void actionPerformed(ActionEvent e) { résultat.setText(prénom.getText()+' '+nom.getText()); } public static void main(String[] args) { new Champ(); } }
La classe Insets permet de spécifier des marges intérieures près du bord, respectivement en haut, à gauche, en bas et à droite.
.
Nous allons maintenant voir comment assurer le suivi, en temps réel, des modifications dans les champs de texte. Pour cela, nous allons mettre en oeuvre une horloge avec deux champs de texte qui permettent de saisir les heures et les minutes. Dès que le contenu des champs est modifié, l'horloge est mise à l'heure.

Garder la trace de tout changement intervenant dans les champs de texte nécessite des efforts supplémentaires. Tout d'abord sachez que surveiller les frappes du clavier ne suffit pas. Certaines touches, telles que les touches fléchées, ne modifient pas le texte.
Nous l'avons abordé au début de cette étude, le champ de texte Swing est implémenté via une méthode générique : la chaîne que vous voyez dans le champ n'est qu'une manifestation visuelle (la vue) d'une structure de données sous-jacente (le modèle). Bien sûr, pour un simple champ de texte, il n'existe pas de différence importante entre ces deux concepts. La vue est une chaîne affichée et le modèle est un objet chaîne. Toutefois, c'est cette même architecture qui est utilisée dans les composants d'édition plus avancés pour présenter du texte formaté avec des polices, des paragraphes et d'autres attributs, représentés en interne par une structure de données plus complexe.
saisie.getDocument().addDocumentListener(écouteur);
Lorsque le texte a changé, l'une des méthodes DocumentListener suivante est appelée :
void insertUpdate(DocumentEvent événement); void removeUpdate(DocumentEvent événement);
void changedUpdate(DocumentEvent événement);
Les deux premières méthodes sont appelées lorsque des caractères ont été insérés ou supprimés. La troisième méthode n'est pas appelée pour les champs de texte. Pour des documents plus comlexes, elle sera appelée pour certains types de modification, tel qu'un changement de mise en forme. Malheureusement, il n'existe pas de méthode de rappel unique pour vous indiquer que le texte a été modifié - généralement, vous ne vous préoccupez pas de la façon dont il a changé.
Il n'existe pas non plus de classe Adapter. Ainsi, l'écouteur de document doit implémenter les trois méthodes.
.
package horloge; import java.awt.*; import java.awt.event.*; import java.text.DateFormat; import java.util.Calendar; import javax.swing.*; import javax.swing.border.EtchedBorder; import javax.swing.event.*; public class Champ extends JFrame implements ActionListener { private Timer minuteur = new Timer(1000, this); private JLabel horloge = new JLabel(); private JPanel panneau = new JPanel(); private Saisie heure; private Saisie minutes; private Calendar date = Calendar.getInstance(); public Champ() { super("Horloge"); setBounds(100, 100, 220, 100); setDefaultCloseOperation(EXIT_ON_CLOSE); horloge.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32)); horloge.setHorizontalAlignment(JLabel.CENTER); horloge.setBorder(new EtchedBorder()); add(horloge); minuteur.start(); panneau.add(new JLabel("Heure :")); panneau.add(heure = new Saisie(""+date.get(Calendar.HOUR_OF_DAY))); panneau.add(new JLabel("Minutes :")); panneau.add(minutes = new Saisie(""+date.get(Calendar.MINUTE))); add(panneau, BorderLayout.SOUTH); setResizable(false); setVisible(true); } public void actionPerformed(ActionEvent e) { date.add(Calendar.SECOND, 1); horloge.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(date.getTime())); } private class Saisie extends JTextField implements DocumentListener { public Saisie(String libellé) { super(libellé, 3); setHorizontalAlignment(RIGHT); getDocument().addDocumentListener(this); } public void insertUpdate(DocumentEvent e) { changement(); } public void removeUpdate(DocumentEvent e) { changement(); } public void changedUpdate(DocumentEvent e) { } private void changement() { try { int h = Integer.parseInt(heure.getText().trim()); int m = Integer.parseInt(minutes.getText().trim()); date.set(Calendar.HOUR_OF_DAY, h); date.set(Calendar.MINUTE, m); } catch (NumberFormatException erreur) {} } } public static void main(String[] args) { new Champ(); } }
Ce code ne fonctionnera toutefois pas correctement si l'utilisateur tape une chaîne telle que "deux", qui ne représente pas un chiffre entier, ou s'il laisse le champ de texte vierge. La méthode parseInt() déclenche alors l'exception NumberFormatException qu'il faut capturer. Ici, l'horloge n'est tout simplement pas mis à jour si l'utilisateur n'entre pas un nombre.
Au lieu d'écouter les événements de document, vous pouvez aussi ajouter un écouteur d'action pour un champ de texte. Celui-ci est notifié lorsque l'utilisateur appuie sur la touche Entrée.
private class Saisie extends JTextField implements ActionListener { public Saisie(String libellé) { super(libellé, 3); setHorizontalAlignment(RIGHT); addActionListener(this); } public void actionPerformed(ActionEvent e) { try { int h = Integer.parseInt(heure.getText().trim()); int m = Integer.parseInt(minutes.getText().trim()); date.set(Calendar.HOUR_OF_DAY, h); date.set(Calendar.MINUTE, m); } catch (NumberFormatException erreur) {} } }
Parfois, vous avez besoin de recueillir une entrée d'utilisateur d'une longueur supérieure à une ligne. JTextArea affiche ainsi plusieurs lignes de texte simple non formaté et permet à l'utilisateur d'éditer ce texte. Lorsque vous placez un composant de ce type dans votre programme, un utilisateur peut taper n'importe quel nombre de lignes de texte en utilisant la touche Entrée pour les séparer. Chaque ligne se termine par un caractère de retour de ligne '\n'.
JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacuneoù le paramètre de colonne qui indique le nombre de colonne fonctionne comme auparavant ; vous devez toujours ajouter quelques colonnes (caractères) supplémentaires par précaution.
L'utilisateur n'est pas limité au nombre de lignes et de colonnes ; le texte défilera si l'entrée est supérieure aux valeurs spécifiées. Vous pouvez également modifier le nombre de colonnes et de lignes en utilisant, respectivement, les méthodes setColumns() et setRows(). Les valeurs données n'indiquent qu'une préférence, le gestionnaire de mise en forme peut toujours agrandir ou réduire la zone de texte.
saisie.setLineWrap(true); // sauts de ligne automatiqueCe renvoi à la ligne n'est qu'un effet visuel. Le texte dans le document n'est pas modifié, aucun caractère '\n' n'est inséré.
JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacuneLe panneau de défilement gère ensuite la vue de la zone de texte. Des barres de défilement apparaissent automatiquement si le texte entré dépasse la zone d'affichage ; elles disparaissent si, lors d'une suppression, le texte restant tient dans la zone de texte. Le défilement est géré en interne par le panneau de défilement - votre programme n'a pas besoin de traiter les événements de défilement.
JScrollPane ascenceur = new JScrollPane(saisie);
C'est un mécanisme général que vous rencontrerez fréquemment en travaillant avec Swing. Pour ajouter des barres de défilement à un composant, placez-le à l'intérieur d'un panneau de défilement.
Par défaut, JTextField et JTextArea sont éditables ; nous pouvons y écrire et modifier du texte. Ces deux composants peuvent être changés en lecture seule en appelant la méthode setEditable(false).
Tous deux supportent également les sélections. Une sélection est une portion de texte mise en inverse vidéo et pouvant être copié, coupée ou collée dans votre système de fenêtrage. Vous sélectionnez le texte avec la souris ; vous pouvez alors le couper, le copier et le coller dans une autre fenêtre en utilisant des raccourcis clavier. Dans la plupart des systèmes, nous utilisons Ctrl-C pour copier, Ctrl-V pour coller et Ctrl-X pour couper. Il est également possible de gérer ces opérations par programme en utilisant les méthodes cut(), copy() et paste() de JTextComponent.
La sélection de texte courante est renvoyée par getSelectedText(), et vous pouvez définir la sélection en utilisant selectText() avec un indice ou bien selectAll().
Je vous propose, à titre d'exemple, de mettre en oeuvre un éditeur de texte simple du même style que le bloc-note de Windows. J'en profite pour implémenter la plupart des méthodes intéressantes issues de la classe parente JTextComponent, savoir :

package editeur; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.event.*; public class Editeur extends JFrame { private Actions actionNouveau = new Actions("Nouveau", "Tout effacer dans la zone d'édition"); private Actions actionOuvrir = new Actions("Ouvrir", "Ouvrir le fichier texte"); private Actions actionEnregistrer = new Actions("Enregistrer", "Sauvegarder le texte"); private Actions actionCopier = new Actions("Copier", "Copier le texte sélectionné"); private Actions actionCouper = new Actions("Couper", "Couper le texte sélectionné"); private Actions actionColler = new Actions("Coller", "Coller à l'emplacement du curseur"); private JMenuBar menu = new JMenuBar(); private JMenu fichier = new JMenu("Fichier"); private JMenu édition = new JMenu("Edition"); private JPanel panneau = new JPanel(); private JTextField positions = new JTextField(" Lignes : 1 Colonnes : 1"); private JTextField lireSélection = new JTextField(24); private ZoneEdition éditeur = new ZoneEdition(); public Editeur() { super("Nouveau document"); setDefaultCloseOperation(EXIT_ON_CLOSE); actionEnregistrer.setEnabled(false); add(new JScrollPane(éditeur)); positions.setEditable(false); panneau.add(positions); panneau.add(new JLabel(" Sélection :")); lireSélection.setEditable(false); lireSélection.setMargin(new Insets(0, 3, 0, 3)); panneau.add(lireSélection); add(panneau, BorderLayout.SOUTH); menu(); pack(); setVisible(true); } private void menu() { setJMenuBar(menu); menu.add(fichier); fichier.add(actionNouveau); fichier.add(actionOuvrir); fichier.add(actionEnregistrer); menu.add(édition); JMenuItem sélection = new JMenuItem("Tout sélectionner"); édition.add(sélection); sélection.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { éditeur.selectAll(); } }); édition.add(actionCopier); édition.add(actionCouper); édition.add(actionColler); final JCheckBoxMenuItem lectureSeule = new JCheckBoxMenuItem("Lecture seule"); édition.add(lectureSeule); lectureSeule.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { éditeur.setEditable(!lectureSeule.isSelected()); } }); } private class ZoneEdition extends JTextArea implements DocumentListener, CaretListener { public ZoneEdition() { super(15, 36); setMargin(new Insets(3,3,3,3)); // marge intérieure setBackground(Color.YELLOW); setForeground(Color.BLUE); setFont(new Font("Verdana", Font.BOLD, 13)); setSelectedTextColor(Color.YELLOW); // couleur rouge sur le texte sélectionné setSelectionColor(Color.RED); // couleur jaune sur le fond du texte sélectionné setCaretColor(Color.BLUE); // curseur de texte en bleu setTabSize(3); getDocument().addDocumentListener(this); addCaretListener(this); } public void insertUpdate(DocumentEvent e) { actionEnregistrer.setEnabled(true); } public void removeUpdate(DocumentEvent e) { actionEnregistrer.setEnabled(true); } public void changedUpdate(DocumentEvent e) { } public void caretUpdate(CaretEvent e) { int lignes = 1; int colonnes = 1; for (int i=0; i<getCaretPosition(); i++) { if (getText().charAt(i)=='\n') { lignes++; colonnes=1; } else colonnes++; } positions.setText(" Lignes : "+lignes+" Colonnes : "+colonnes); lireSélection.setText(getSelectedText()); panneau.revalidate(); } } private class Actions extends AbstractAction { private String méthode; private JFileChooser boîte = new JFileChooser(); public Actions(String libellé, String description) { super(libellé, new ImageIcon(Editeur.class.getResource(libellé.toLowerCase()+".gif"))); putValue(SHORT_DESCRIPTION, description); putValue(MNEMONIC_KEY, (int)libellé.charAt(0)); méthode = libellé.toLowerCase(); } public void actionPerformed(ActionEvent e) { try { this.getClass().getDeclaredMethod(méthode).invoke(this); } catch (Exception ex) { setTitle("Problème");} } private void nouveau() { setTitle("Nouveau document"); éditeur.setText(""); actionEnregistrer.setEnabled(false); } private void ouvrir() throws IOException { if (boîte.showOpenDialog(Editeur.this)==JFileChooser.APPROVE_OPTION) { setTitle(boîte.getSelectedFile().getName()); éditeur.read(new FileReader(boîte.getSelectedFile()), null); } } private void enregistrer() throws IOException { if (boîte.showSaveDialog(Editeur.this)==JFileChooser.APPROVE_OPTION) { setTitle(boîte.getSelectedFile().getName()); éditeur.write(new FileWriter(boîte.getSelectedFile())); } } private void copier() throws IOException { éditeur.copy(); } private void couper() throws IOException { éditeur.cut(); } private void coller() throws IOException { éditeur.paste(); } } public static void main(String[] args) { new Editeur(); } }
J'aimerais rester quelques instants sur l'architecture Modèle-Vue-Contrôleur. L'exemple que je vous propose ci-dessous montre combien il est facile de partager un même Document par plusieurs composants. En effet, je reprends le projet précédent auquel je rajoute une autre zone de saisie qui est liée à la première puisqu'elles utilisent le même modèle de données. Ainsi, nous pouvons consulter à la fois le début et la fin d'un document grâce à ces deux vues différentes associées à ce même texte.
Nous pouvons agir aussi bien sur une vue que sur l'autre. Ainsi, la sélection peut se faire indifféremment sur la première zone de texte ou sur la deuxième.

deuxième.setDocument(premier.getDocument()); // mise en place d'un document commun pour deux vues différentes
... private JTextField lireSélection = new JTextField(24); private ZoneEdition éditeur = new ZoneEdition(); private ZoneEdition deuxième = new ZoneEdition(); public Editeur() { super("Nouveau document"); setDefaultCloseOperation(EXIT_ON_CLOSE); actionEnregistrer.setEnabled(false); add(new JScrollPane(éditeur), BorderLayout.NORTH); deuxième.setDocument(éditeur.getDocument()); add(new JScrollPane(deuxième)); positions.setEditable(false); ...
Le champ de mot de passe représenté par JPasswordField est un type spécial de champ de texte (sous-classe de JTextField). Il est conçu pour saisir des mots de passe et d'autres donnée sensibles. Pour éviter que des voisins curieux ne puissent s'apercevoir le mot de passe entré par un utilisateur, les caractères tapés ne sont pas affichés. Un caractère d'écho est utilisé à la place, généralement un astérisque (*). Eventuellement, la méthode setEchoChar() permet de choisir le caractère d'écho à faire apparaître au lieu des caractères entrés par l'utilisateur.
Le champ de mot de passe est un autre exemple de la puissance de l'architecture Modèle-Vue-Contrôleur. Il utilise le même modèle qu'un champ de texte standard pour conserver les données, mais sa vue a été modifiée pour n'afficher que des caractères d'écho.
Normalement, getText() permet de récupérer le texte saisi dans le champ de mot de passe, mais cette méthode est devenue désuète. Vous devez plutôt utiliser getPassword(), qui renvoie un tableau de caractères et non un objet String. Les tableaux de caractères sont moins vulnérables que les String face aux programmes renifleurs de mots de passe dans la mémoire. Si cela ne vous concerne pas outre mesure, vous pouvez créer un nouveau String à partir du tableau de caractères. Remarquez que les méthodes des classes de cryptographie de Java acceptent les mots de passe sous forme de tableaux de caractères, non sous forme de chaînes ; il est donc très cohérent de transmettre le résultat d'un appel à getPassword() directement aux méthodes des classes cryptographiques, sans créer le moindre String.
package saisie; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Champ extends JFrame implements ActionListener { private JLabel intituléId = new JLabel("Identifiant :"); private JTextField identifiant = new JTextField(20); private JLabel intituléPasse = new JLabel("Mot de passe :"); private JPasswordField passe = new JPasswordField(); private JButton validation = new JButton("Valider"); public Champ() { super("Saisie des références"); // passe.setEchoChar('#'); gestionDisposition(); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); setVisible(true); validation.addActionListener(this); } private void gestionDisposition() { GroupLayout groupe = new GroupLayout(getContentPane()); getContentPane().setLayout(groupe); groupe.setAutoCreateContainerGaps(true); groupe.setAutoCreateGaps(true); GroupLayout.ParallelGroup horzGroupe = groupe.createParallelGroup(); GroupLayout.SequentialGroup vertGroupe = groupe.createSequentialGroup(); horzGroupe.addComponent(intituléId).addComponent(identifiant).addComponent(intituléPasse).addComponent(passe).addComponent(validation); vertGroupe.addComponent(intituléId).addComponent(identifiant).addComponent(intituléPasse).addComponent(passe).addComponent(validation); groupe.setHorizontalGroup(horzGroupe); groupe.setVerticalGroup(vertGroupe); } public void actionPerformed(ActionEvent e) { setTitle(identifiant.getText()+" : "+String.valueOf(passe.getPassword())); } public static void main(String[] args) { new Champ(); } }
Dans les saisies, nous avons souvent besoin de récupérer des valeurs numériques, une suite de chiffres et non des chaînes de caractères arbitraires. Dans ce cas là, l'utilisateur n'est autorisé à taper que des chiffres de 0 à 9 et un signe moins "-". Si ce signe est utilisé, il doit représenter le premier caractère de la chaîne d'entrée.
En apparence, la validation d'entrée semble simple. Nous pouvons mettre en oeuvre un écouteur de touche pour le champ de texte et bloquer tous les événements des touches qui ne représentent pas un chiffre ou un signe moins. Malheureusement, cette approche simple, bien que recommandée comme méthode de validation d'entrée, ne fonctionne pas bien dans la pratique. Tout d'abord, certaines associations de touches autorisées ne constituent pas obligatoirement une entrée valide, par exemple, - -3 ou 3 - 3.
Plus important encore, il existe d'autres moyens de modifier le texte qui ne font pas appel à la pression d'une touche. Selon le style d'interface implémenté, certaines combinaisons de clavier peuvent servir pour couper, copier ou coller du texte. Pour cette raison, nous devrions aussi nous assurer que l'utilisateur ne colle pas de caractères invalides. Bref, cette tentative de filtrer les frappes du clavier pour valider une entrée commence à devenir complexe.
Heureusement, il existe une classe, JFormattedTextField, qui palie à ce genre de problème. En effet, ce composant offre un support explicite pour éditer des valeurs formatées complexes comme les chiffres, mais également les dates, et des mises en forme plus ésotériques, comme les adresses IP.
JFormattedTextField agit un peu comme JTextField, sauf qu'il accepte dans son constructeur un objet spécifiant le format et gère un type d'objet complexe (comme Date ou Integer) via ses méthodes setValue() et getValue(). L'exemple qui suit montre la construction d'un simple écran avec plusieurs types de champ formatés :
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import java.util.Date; import javax.swing.text.MaskFormatter; public class TexteFormaté extends JFrame implements ActionListener { private SimpleDateFormat formatDate = new SimpleDateFormat("dd/MM/yyyy"); private JFormattedTextField anniversaire = new JFormattedTextField(formatDate); private JFormattedTextField âge = new JFormattedTextField(NumberFormat.getIntegerInstance()); private JFormattedTextField téléphone; private Box groupe = Box.createVerticalBox(); public TexteFormaté() throws ParseException { super("Saisie"); setSize(250, 150); groupe.add(new JLabel("Date anniversaire :")); anniversaire.setValue(new Date()); anniversaire.addActionListener(this); groupe.add(anniversaire); groupe.add(new JLabel("Âge : ")); âge.setValue(new Integer(48)); âge.addActionListener(this); groupe.add(âge); groupe.add(new JLabel("Téléphone :")); téléphone = new JFormattedTextField(new MaskFormatter("0#.##.##.##.##")); téléphone.setValue("04.71.63.55.08"); téléphone.addActionListener(this); groupe.add(téléphone); add(groupe); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { Date date = (Date)anniversaire.getValue(); Number nombre = (Number)this.âge.getValue(); int âge = nombre.intValue(); String téléphone = (String)this.téléphone.getValue(); setTitle(formatDate.format(date)+" : "+âge+" : "+téléphone); } public static void main(String[] args) throws ParseException { new TexteFormaté(); } }
Reportez-vous à l'étude suivante - Formater nombre et date - pour prendre connaissance plus précisément sur ces différents type de formatage.
.
La construction étant faite, vous pouvez définir une valeur valide en utilisant setValue() et récupérer la dernière valeur valide avec getValue(). Pour ce faire, vous devez transtyper la valeur dans le bon type, basé sur le format que vous utilisez. Par exemple, cette commande récupère la date du champ anniversaire :
Date date = (Date)anniversaire.getValue();
Un objet de type JFormattedTextField valide le texte lorsque l'utilisateur essaie de transférer le focus sur un nouveau champ (soit en cliquant en dehors du champ, soit en utilisant la navigation du clavier). Par défaut, JFormattedTextField gère les entrées invalides en retournant tout simplement à la dernière valeur valide.
Nous allons maintenant voir en détail l'ensemble des formats que nous pouvons traiter. Nous allons ainsi reprendre cette étude préliminaire en proposant une analyse plus fine. Commençons par un cas facile : un champ de texte pour la saisie d'un entier.
JFormattedTextField champEntier = new JFormattedTextField(NumberFormat.getIntegerInstance());
NumberFormat.getIntegerInstance() renvoie un objet de mise en forme qui formate les entiers à l'aide des paramètres régionaux. Dans les paramètres français, les virgules servent de séparateurs décimaux (ce qui permet de saisir des valeurs comme 1,72), les espaces servent de séparateur de milliers.JFormattedTextField champEntier = new JFormattedTextField(new Integer(37));
Toutefois, l'autoboxing fonctionne tout à fait correctement depuis la version 5 de java, vous pouvez donc écrire directement :JFormattedTextField champEntier = new JFormattedTextField(37);
champEntier.setColumns(20);
champEntier.setValue(new Integer(37));
Encore une fois, vous pouvez utiliser l'autoboxing :champEntier.setValue(37);
Number nombre = (Number)champEntier.getValue();
int entier = nombre.intValue();
Pour connaître les fonctionnalités des classes enveloppes comme Integer, repportez vous à la rubrique suivante : Classes enveloppes.
.
Le champ de texte mis en forme n'est pas très intéressant tant que vous ne pensez pas à ce qui survient lorsque l'utilisateur saisit des données non autorisées. C'est le sujet de la prochaine section.
Imaginons un utilisateur entrant des données dans un champ de texte. Il tape des informations, puis décide finalement de quitter le champ, par exemple en cliquant sur un autre composant. Le champ de texte perd alors le focus (la focalisation). Le curseur en (I) n'y est plus visible et les frappes sur les touches sont destinées à un autre composant.
Lorsque le champ de texte mis en forme perd le focus, l'élément de mise en forme étudie la chaîne de texte produite par l'utilisateur. S'il sait la convertir en objet, le texte est considéré comme valide, sinon il est signalé non valide. Vous pouvez utiliser la méthode isEditValid() pour vérifier la validité du champ de texte.
Le comportement par défaut en cas de perte de focalisation est appelé "commit or revert" (engager ou retourner). Si la chaîne de texte est valide, elle est engagée (commited). Le formateur la transforme en objet, qui devient la valeur actuelle du champ (c'est-à-dire la valeur de retour de la méthode getValue() vue à la section précédente). La valeur est ensuite retransformée en chaîne, qui devient la chaîne de texte visible dans le champ.
Le formateur d'entier reconnaît par exemple que l'entrée 1729 est valide, il définit la valeur actuelle sur new Long(1729), puis la retransforme en chaîne en insérant un espace pour les milliers : 1 729.
A l'inverse, si la chaîne de texte n'est pas valide, la valeur n'est pas modifiée et le champ de texte retourne à la chaîne représentant l'ancienne valeur.
.
Par exemple, si l'utilisateur entre une valeur erronée, comme x1, l'ancienne valeur est récupérée lorsque le champ de texte pert le focus.
Le formateur d'entier considère une chaîne de texte comme valide si elle commence par un entier. Par exemple, 1729x est une chaîne valide. Elle est transformée en 1729, puis mis en forme (1 729).
JFormattedTextField prend bien sûr en charge d'autres formateurs en plus du formateur d'entier. La classe NumberFormat, je le rappelle, possède les méthodes statiques suivantes :
Reportez-vous à l'étude suivante - Format de nombre personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; public class Conversion extends JFrame implements ActionListener { private JFormattedTextField saisie = new JFormattedTextField(NumberFormat.getCurrencyInstance()); private JFormattedTextField résultat = new JFormattedTextField(new DecimalFormat("#,##0.00 F")); public Conversion() { super("Conversion €uro -> Francs"); saisie.setColumns(25); saisie.setValue(0); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); résultat.setValue(0); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } 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 static void main(String[] args) { new Conversion(); } }
Pour éditer les dates et les heures, appelez l'une des méthodes statiques de la classe DateFormat :
Pour la gestion des dates, il est possible de spécifier plus précisément le format désiré. Ainsi, si vous désirez avoir le format :
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.SHORT));
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance()); // ou DateFormat.MEDIUM
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.LONG));
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.FULL));
Par défaut, le format de date est assez clément. Ainsi, une date non valide comme le 31 février 2007 est transformé pour indiquer la prochaine date valide, à savoir le 3 mars 2007. Attention, ce comportement peut surprendre les utilisateurs ! Dans ce cas, appelez setLenient(false) sur l'objet DateFormat.
Reportez-vous à l'étude suivante - Format de date personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import java.util.Date; public class JourSemaine extends JFrame implements ActionListener { private JFormattedTextField saisie = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.SHORT)); private JFormattedTextField résultat = new JFormattedTextField(new SimpleDateFormat("EEEE")); public JourSemaine() { super("Jour de la semaine"); saisie.setColumns(20); saisie.setValue(new Date()); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); résultat.setValue(new Date()); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { résultat.setValue((Date)saisie.getValue()); } public static void main(String[] args) { new JourSemaine(); } }
DefaultFormatter est capable de mettre en forme les objets de toute classe qui disposent d'un constructeur avec un paramètre de chaîne et une méthode toString() correspondante. Par exemple, la classe URL dispose d'un constructeur URL(String) pouvant être utilisé pour construire une URL depuis une chaîne, comme :
URL url = new URL("http://java.sun.com");
Vous pouvez donc utiliser DefaultFormatter pour mettre en forme les objets URL. Le formateur appelle toString() sur la valeur du champ pour initialiser le texte. Lorsque le champ perd le focus, le formateur construit un nouvel objet de la même classe que la valeur actuelle, en utilisant le constrcuteur avec un paramètre String. Si ce constructeur déclenche une exception, la modification n'est pas valide.
Par défaut, DefaultFormatter est en mode overwrite (mode de remplacement). Cette situation est différente pour les autres formateurs et finalement pas très utile. Appelez la méthode setOverwriteMode(false) pour désactiver le mode overwrite.
package format; import java.net.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import javax.swing.text.DefaultFormatter; public class ValidationURL extends JFrame implements ActionListener { private DefaultFormatter format = new DefaultFormatter(); private JFormattedTextField saisie = new JFormattedTextField(format); private JTextField résultat = new JTextField("Saisissez votre adresse URL"); private URL url; public ValidationURL() throws MalformedURLException { super("Saisie de l'URL"); format.setOverwriteMode(false); saisie.setColumns(20); url = new URL("http:"); saisie.setValue(url); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { if (saisie.isEditValid()) { résultat.setText("URL valide"); url = (URL) saisie.getValue(); } else