La saisie et l'édition

Chapitres traités   

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.


Choix du chapitre Entrée et lecture de texte - JTextComponent

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 :

  1. JTextField : collecte une entrée de texte sur une seule ligne.
  2. JPasswordField : spécialisée pour la saisie d'un mot de passe. Ainsi, les valeurs saisies n'apparaissent pas directement dans la zone de champ. Un caractère par défaut (astérisque) est alors affiché à la place de chaque caractère introduit.
  3. JFormattedTextField : collecte une valeur numérique ou une date. Le format de la valeur numérique attendue peut être entièrement paramétrée.
  4. JTextArea : collecte une entrée de texte sur plusieurs lignes.
  5. JEditorPane : permet l'affichage et l'édition de texte complexe formaté comme des documents RTF et HTML, en conjonction avec les classes du paquetage javax.swing.text.html et javax.swing.text.rtf.
  6. JTextPane : hérite de JEditorPane et propose des fonctionnalités supplémentaires. Elle sait notamment afficher plusieurs polices et plusieurs styles dans un même document. Elle gère également un curseur, la mise en évidence, l'incorporation d'image, ainsi que d'autres fonctionnalités élaborées.

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.

Modèle-Vue-Contrôleur

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.

Fonctionnalités de JTextComponent

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) :

Document getDocument()
void setDocument(Document document)
Gestion du document sous-jacent dans le modèle MVC.
int getCaretPosition()
void setCaretPosition(int position)
void moveCaretPosition(int position)
Gestion de la position du curseur de texte permettant de localiser l'endroit où se fait la saisie.
void addCaretListener(CaretListener écouteurPositionCurseur)
Ajoute un écouteur sur le déplacement du curseur de texte. Cet écouteur doit implémenter la méthode unique : void caretUpdate(CaretEvent e);
Color getCaretColor()
void setCaretColor(Color couleur)
Spécifie ou récupère la couleur du curseur de texte.
boolean isEditable()
void setEditable(boolean valider)
Autorise ou empêche la saisie du texte. Dans ce dernier cas, le texte ne peut être que lu.
Insets getMargin()
void setMargin(Insets marge)
Gestion des marges intérieures dans la zone de texte. Si vous désirez changer les marges par défaut, il faut alors proposer un nouvel objet Insets avec la syntaxe suivante :
saisie.setMargin(new Insets(haut, gauche, bas, droit));
String getText()
String getText(int début, int nombreCaractères)
void setText(String texte)
Permet de récupèrer le texte, ou une portion de texte ou d'en proposer un autre.
String getSelectedText()
int getSelectionStart()
int getSelectionEnd()
Récupère du texte à partir d'une sélection.
void select(int début, int fin)
void selectAll()
void setSelectionStart(int début)
void setSelectionEnd(int fin)
Sélectionne du texte.
Color getSelectedTextColor()
void setSelectedTextColor(Color couleur)
Récupère ou change la couleur du texte sélectionné.
Color getSelectionColor()
void setSelectionColor(Color couleur)
Récupère ou change la couleur de la sélection (le fond).
void replaceSelection(String texte)
Propose un remplacement de texte sur la partie sélectionnée.
void copy()
void cut()
void paste()
Copier, couper et coller à partir du presse-papier.
void read(Reader flux, Object description)
void write(Writer flux)
Permet de lire ou de sauvegarder votre texte à partir d'un flux (fichier, réseau, etc.).

Choix du chapitre Champ de texte - JTextField

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 :

  1. setFont() permet de spécifier la fonte dans lequel le texte est affiché.
  2. setColumns() donne le nombre de caractères dans le champ. Remarquez que ce nombre est approximatif à moins d'employer une fonte à chasse constante.

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().

Construction

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().

  1. Il existe un constructeur qui propose un texte par défaut dans la zone de saisie avec le nombre de colonnes souhaitées.
    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.

  2. En général, vous autorisez l'utilisateur à ajouter du texte (ou à modifier le texte existant) dans les champs de texte ; ceux-ci apparaissent donc vides le plus souvent lors du premier affichage. Pour qu'un champ soit vide, il suffit de ne pas passer de chaîne pour le paramètre concerné du constructeur JTextField :

    JTextField saisie = new JTextField(20);

Travailler avec le texte

Les méthodes suivantes permettent de travailler avec le texte de la zone de saisie :

  1. Vous pouvez modifier le contenu du champ de texte à tout moment avec la méthode setText() de la classe parente JTextComponent mentionnée dans le chapitre précédent.
  2. De plus, vous pouvez déterminer ce que l'utilisateur a tapé en appelant la méthode getText(). Elle renvoie tout ce que l'utilisateur a entré, y compris les espaces en tête et en fin de chaîne. Vous pouvez les supprimer grâce à la méthode trim() lors de la récupération de l'entrée :

    String texte = saisie.getText().trim();

  3. Pour modifier la police du texte saisie par l'utilisateur, appelez la méthode setFont().

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.

Exemple d'école

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 :

code correspondant
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.
.

 

Choix du chapitre Suivi des modifications dans les champs de texte

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.

Retour sur le modèle MVC

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.

Le modèle pour tous les composants texte est décrit par l'interface Document qui concerne aussi bien du texte simple que formaté, comme HTML. En fait, vous pouvez interroger le document (et non le composant texte) pour être informé des changements, en prévoyant un écouteur de document :

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.
.

codage correspondant
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.

Ecouteur de type ActionListener

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.

partie modifié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) {}         
      }
   }

 

Choix du chapitre Zone de texte - JTextArea

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'.

  1. Création d'un zone de texte : dans le constructeur du composant JTextArea, vous spécifiez le nombre de lignes et de colonnes pour la zone :
    JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacune
    où 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.

  2. Retour à la ligne automatique : si le texte entré dépasse la capacité d'affichage de la zone de texte, le reste du texte est coupé. Pour éviter que des longues lignes ne soient tronquées, vous pouvez activer le retour automatique à la ligne avec la méthode setLineWrap() :
    saisie.setLineWrap(true); // sauts de ligne automatique
    Ce 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é.
  3. Barres de défilement : dans Swing, une zone de texte ne dispose pas de barres de défilement. Si vous souhaitez en ajouter, prévoyer la zone de texte dans un panneau avec barre de défilement JScrollPane :
    JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacune
    JScrollPane ascenceur = new JScrollPane(saisie);
    Le 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.

    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.

  4. Recenser le nombre de lignes que comporte le texte : il est possible de connaître le nombre total de lignes présentes dans la zone d'édition au moyen de la méthode getLineCount().
  5. Gestion de la tabulation : les méthodes getTabSize() et setTabSize() permettent récupérer ou de spécifier une nouvelle valeur de tabulation (8 par défaut).
  6. Nouvelle gestion de texte : la méthode append(texte) permet de rajouter du texte à la fin de celui qui est déjà présent. La méthode insert(texte, position) permet elle de placer du texte à l'endroit spécifié alors que la méthode replaceRange(texte, début, fin) remplace le texte par rapport aux bornes proposées.

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().

Exemple qui prend en compte les méthodes issues de la super-classe JTextComponent

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 :

  1. Permettre l'édition ou provoquer la lecture seule : setEditable()
  2. Proposer des marges : setMargin()
  3. Prendre en compte le déplacement du curseur de texte : addCaretListener() -> updateCaret()
  4. Changer les couleurs de la sélection : setSelectedTextColor() et setSelectionColor()
  5. Changer la couleur du curseur de texte : setCaretColor()
  6. Récupérer la sélection : getSelectedText()
  7. Enregistrer ou lire un fichier texte : write() et read()
  8. Copier, couper et coller : copy(), cut() et paste().


Codage correspondant
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(); }
}

 

Choix du chapitre Partage d'un modèle de données

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.


La mise en oeuvre de cette technique consiste à utiliser les méthodes getDocument() et setDocument() héritées de la classe de base JTextComponent. Il suffit en effet de proposer un document à la deuxième zone de texte en le récupérant de la première zone de texte, et le tour est joué :
deuxième.setDocument(premier.getDocument()); // mise en place d'un document commun pour deux vues différentes
Modification du code en conséquence
 ...
    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);
...

 

Choix du chapitre Champ de mot de passe - JPasswordField

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.

Cas d'école
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(); }
}

 

Choix du chapitre Champ de saisie mis en forme - JFormattedTextField

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.

Choix des formats utilisés

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 :

Exemple de champs 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é(); }   
}
Un objet de type JFormattedTextField peut être construit avec différents objets spécifiant des formats, notamment java.lang.Number (par exemple Integer et Double), java.text.Format, java.text.DateFormat et le plus arbitraire java.text.MaskFormatter. Nous pouvons également prévoir de nouveaux format personnalisés à l'aide de la classe java.text.DefaultFormatter.

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.

Saisie d'entiers - Formateur d'entier - NumberFormat

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.

  1. Création du champ en format 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.

    Il est également possible de créer une instance avec une valeur par défaut. Passez directement, dans ce cas là, par la classe Integer qui sert de classe enveloppe pour l'entier désiré :

    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);

  2. Définir le nombre de colonnes : Comme pour tout champ de texte, vous pouvez définir le nombre de colonnes, tout simplement par la méthode setColumns() :

    champEntier.setColumns(20);

  3. Proposer une nouvelle valeur par défaut : La méthode setValue() peut être accompagnée d'une valeur par défaut. Elle prend un paramètre Object, vous devrez donc envelopper la valeur int par défaut dans un objet Integer :

    champEntier.setValue(new Integer(37));

    Encore une fois, vous pouvez utiliser l'autoboxing :

    champEntier.setValue(37);

  4. Récupération des valeurs : Les utilisateurs saisissent généralement des informations dans plusieurs champs de texte, puis cliquent sur un bouton pour lire toutes les valeurs. Après ce clic, vous pouvez récupérer la valeur fournie par l'utilisateur grâce à la méthode getValue(). Elle renvoie un résultat Object et vous devez la transtyper dans le type approprié. JFormattedTextField renvoie un objet de type Long si l'utilisateur a modifier la valeur et l'objet Integer initial si ce n'est pas le cas. Il vous faut donc transtyper la valeur de retour sur la super classe Number habituelle :

    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.

Comportement en cas de perte de focalisation

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).

Autres formateurs standard - valeurs numériques - NumberFormat et DecimalFormat

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 :

  1. getNumberInstance() : pour les nombres à virgule flottante.
  2. getCurrencyInstance() : pour les valeurs monnétaires du pays. En France, c'est l'€uro.
  3. getPercentInstance() : pour les pourcentages.
Il est également possible de prévoir un format numérique tout à fait personnalisé au moyen de la classe DecimalFormat.

Reportez-vous à l'étude suivante - Format de nombre personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.

Exemple d'application avec une conversion entre les €uros et les francs
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(); }   
}

Autres formateurs standard - dates - DateFormat et SimpleDateFormat

Pour éditer les dates et les heures, appelez l'une des méthodes statiques de la classe DateFormat :

  1. getDateInstance() : adaptée au formatage des dates selon les paramètres locaux spécifiés ou par défaut.
  2. getTimeInstance() : formate et analyse les heures.
  3. getDateTimeInstance() : formate à la fois les dates et les heures.

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 :

  1. Court : 22/11/07

    JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.SHORT));

  2. Moyen (par défaut) : 22 nov. 2007

    JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance()); // ou DateFormat.MEDIUM

  3. Long : 22 novembre 2007

    JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.LONG));

  4. Complet : jeudi 22 novembre 2007

    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.

Il est également possible de prévoir un format de date tout à fait personnalisé au moyen de la classe SimpleDateFormat.

Reportez-vous à l'étude suivante - Format de date personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.

Retrouver le jour de la semaine à partir de la saisie d'une date
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(); }   
}

Format par défaut - DefaultFormatter

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.

Nous pouvons tester cette situation en entrant une URL qui ne commence pas par un préfixe comme http:
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