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 résultat.setText("URL non valide");
   }
   
   public static void main(String[] args) throws MalformedURLException  { new  ValidationURL(); }   
}

Masque de format - MaskFormatter

Il existe un dernier format qui permet de mettre en place des masques de saisie. MaskFormatter convient bien aux motifs à taille fixe qui contiennent des caractères constants et des caractères variables.

Les numéros de sécurité sociale, par exemple (comme 1-56-08-75-205-082-56), peuvent être mis en forme de la manière suivante :

new MaskFormatter("#-##-##-###-###-##"); // le symbole # remplace un chiffre (masque pour les chiffres)

Symboles de MaskFormatter
# Un chiffre
? Une lettre
U Une lettre, transformée en majuscule
L Une lettre, transformée en minuscule
A Une lettre ou un chiffre
H Un chiffre hexadécimal [0-9A-Fa-f]
* Tout caractère
' Caractère d'échappement pour inclure un symbole dans le motif
Vous pouvez limiter les caractères pouvant être tapés dans le champ en appelant l'une des méthodes de la classe MaskFormatter : setValidCharacters() ou/et setInvalidCharacters(). Par exemple, pour lire une note scolaire exprimée par une lettre (comme A+ ou F), vous pourriez utiliser :

MaskFormatter masque = new MaskFormatter("U*");
masque.setValidCharacters("ABCDEF+- ");

Il n'existe toutefois aucune méthode permettant d'indiquer que le deuxième charactère ne doit pas être une lettre.
.

Sachez que la chaîne de mise en forme par le formateur du masque a exactement la même longueur que le masque. Si l'utilisateur efface des caractères pendant la modification, ceux-ci sont remplacés par le caractère d'emplacement. Ce caractère est, par défaut, un espace, mais vous pouvez le modifier grâce à la méthode setPlaceholderCharacter() :

masque.setPlaceholderCharacter('0');

Vous pouvez également proposer toute une chaîne de caractères qui s'affiche par défaut à l'aide de la méthode setPlaceholder() :

masque.setPlaceholder("0000 0000");

Par défaut, un formateur de masque agit en mode recouvrement, un mode assez intuitif. Sachez également que la position du curseur passe au-dessus des caractères fixes sur le masque.

Le formateur de masque est très efficace pour les motifs rigides comme les numéros de sécurité sociale ou les numéros de téléphone. Sachez qu'aucune variation n'est admise dans le motif du masque. Vous ne pouvez pas, par exemple, utiliser un formateur de masque pour les numéros de téléphones internationaux, dont le nombre de chiffres varie.

Exemple qui permet de saisir des nombres binaires avec un séparateur de quartet :

Code de l'application
package format;

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

public class Binaire extends JFrame implements ActionListener {
   private MaskFormatter masque; 
   private JFormattedTextField saisie;
   private JFormattedTextField résultat = new JFormattedTextField(0);
   
   public Binaire() throws ParseException {
      super("Binaire");
      setLayout(new FlowLayout());
      add(new JLabel("Binaire :"));
      masque = new MaskFormatter("#### ####");
      masque.setValidCharacters("01");
      masque.setPlaceholder("0000 0000");
      saisie = new JFormattedTextField(masque);
      add(saisie);
      add(new JLabel("Décimal :"));
      résultat.setEditable(false);
      résultat.setColumns(5);
      add(résultat);
      saisie.addActionListener(this);
      pack();
      setResizable(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public void actionPerformed(ActionEvent e) {
      StringBuilder chaîne = new StringBuilder((String) saisie.getValue());
      chaîne.deleteCharAt(4);
      résultat.setValue(Integer.parseInt(chaîne.toString(), 2));
   }
   
   public static void main(String[] args) throws ParseException  { new Binaire(); }   
}

Pour connaître les fonctionnalités de StringBuilder, repportez vous à la rubrique suivante :
Manipulation sur la même chaîne.
Pour connaître les fonctionnalités des classes enveloppes comme Integer, repportez vous à la rubrique suivante :
Classes enveloppes.
Pour connaître les conversions entre les nombres et les chaînes de caractères :
Conversions.

Format personnalisé - retour sur la classe DefaultFormatter

Lorsqu'aucun des formateurs standard ne convient, vous pouvez assez facilement définir le vôtre. Envisagez des adresses IP à 4 octets, comme 130.65.86.66. Vous ne pouvez pas utiliser un MaskFormatter car chaque octet pourrait être représenté par un, deux ou trois chiffres. Il faut également s'assurer que la valeur de chaque octet ne dépasse pas 255.

Pour personnaliser votre formateur, il suffit d'hériter de la classe DefaultFormatter et de redéfinir les méthodes valueToString() et stringToValue() :
  1. String valueToString(Object valeur) : transforme la valeur du champ en chaîne qui s'affiche dans le champ de texte.
  2. Object stringToValue(String texte) : analyse le texte tapé par l'utilisateur et le retransforme en objet.

Si l'une ou l'autre méthode détecte une erreur, elle lance une exception ParseException.
.

codage de la saisie d'une adresse IP valide
package format;

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

public class AdresseIP extends JFrame implements ActionListener {
   private JFormattedTextField saisie;
   private JTextField résultat = new JTextField("Saisissez votre adresse IP");
   
   public AdresseIP() {
      super("Binaire"); 
      saisie = new JFormattedTextField(new FormatIP());
      saisie.setValue(new byte[] {(byte)172, 16, 40, 56});
      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) {
      byte[] octets = (byte[]) saisie.getValue();
      StringBuilder chaîne = new StringBuilder();
      for (byte octet : octets) chaîne.append(octet+" ");
      résultat.setText("Adresse IP : "+chaîne.toString());
   }
   
   private class FormatIP extends DefaultFormatter {
      public FormatIP() {  setOverwriteMode(false); }
      
      @Override
      public Object stringToValue(String chaîne) throws ParseException {
         byte[] octets = new byte[4];
         Scanner nombres = new Scanner(chaîne);
         nombres.useDelimiter("\\.");
         int indice = 0;
         while (nombres.hasNextInt()) {
             int nombre = nombres.nextInt();
             if (nombre<0 || nombre>=256) throw new ParseException("Le champ doit être compris entre 0 et 255", 0); 
             octets[indice++] = (byte) nombre;
         }
         if (indice != 4) throw new ParseException("Il faut quatre octets à l'adresse IP", 0);
         return octets;         
      }

      @Override
      public String valueToString(Object tableau) throws ParseException {
         if (!(tableau instanceof byte[])) throw new ParseException("ll faut un tableau d'octets", 0);
         byte[] octets = (byte[]) tableau;
         if (octets.length!=4) throw new ParseException("ll faut quatre octets à l'adresse IP", 0);
         StringBuilder chaîne = new StringBuilder();
         for (int i=0; i<4; i++) {
            int nombre = octets[i];
            if (nombre<0) nombre+=256;
            chaîne.append(""+nombre);
            if (i<3) chaîne.append(".");
         }
         return chaîne.toString();
      }      
   }
   
   public static void main(String[] args) { new AdresseIP(); }   
}

La méthode valueToString() n'appelle pas de commentaire particulier. Par contre, dans la méthode stringToValue(), vous remarquez que nous utilisons la classe Scanner pour récupérer chacun des champs. Il suffit de passer la chaîne de caractères en argument du constructeur. Cette classe possède la particularité de pouvoir récupérer des valeurs de différents types comme des valeurs numériques.

Vous devez spécifier un élément séparateur afin de pouvoir passer d'une partie de chaîne à l'autre, grâce à la méthode useDelimiter(). Cette méthode attend le motif d'une expression régulière. Attention, le caractère (.) est interprété comme étant tout caractère de chaîne dans une expression régulière, il faut donc rajouter le caractère antislash (\) pour indiquer qu'il s'agit d'un simple caractère (non interprétable). Malheureusement, le caractère antislash est également interprété dans les expressions régulières. Pour éviter tout conflit, il suffit de prendre un autre antislash. Finalement, voici tout ce qu'il faut mettre pour dire que le point sert d'élément séparateur "\\.".

Pour connaître les différentes particularités de la classe Scanner, revoyez l'étude suivante : Décomposition de texte à l'aide de la classe Scanner.
Pour revenir sur les expressions régulières : Mise en correspondance de motifs à l'aide d'expressions régulières.

Toujours pour la méthode stringToValue(), nous pouvons passer aussi par la classe StringTokenizer qui est spécialisée pour découper du texte (token) à partir d'un élément de séparation appelé délimiteur.

Modification correspondante
@Override
public Object stringToValue(String chaîne) throws ParseException {
    byte[] octets = new byte[4];
    StringTokenizer parties = new StringTokenizer(chaîne, ".");
    if (parties.countTokens()!=4) throw new ParseException("Il faut quatre octets à l'adresse IP", 0); 
    for (int i=0; i<4; i++) {
        int nombre = 0;
        try {
            nombre = Integer.parseInt(parties.nextToken());
        }
        catch (NumberFormatException e) { throw new ParseException("Il faut un tableau d'octets", 0); }
        if (nombre<0 || nombre>=256) throw new ParseException("Le champ doit être compris entre 0 et 255", 0);
        octets[i] = (byte) nombre;
    }
    return octets;         
}

Pour en savoir plus sur la découpe de texte, revoyez l'étude suivante : Utilisation de délimiteurs pour décomposer du texte.
.

 

Choix du chapitre Filtrer les entrées

Après avoir pris connaissances de l'ensemble des saisies possibles, nous allons maintenant nous intéresser plus particulièrement sur le filtrage des entrées afin que la saisie faite par l'utilisateur soit adaptée de façon fine au traitement souhaité par l'application interne. Grâce au chapitre précédent, nous avons déjà découvert qu'il est possible de prendre en compte des valeurs suivant un format spécifique. Cette fonction de base des champs de texte mis en forme est simple et suffit dans la plupart des cas. Vous pouvez toutefois affiner quelque peut le processus. Il est possible, par exemple, d'empêcher l'utilisateur d'entrer d'autres caractères que les chiffres. Pour ce faire, vous utiliserez un filtre de document.

En réalité, JFormattedTextField ne connaît pas lui-même tous les types de format, pour avoir connaissance de formats particuliers, il utilise les objets AbstractFormatter. En retour, les AbstractFormatter fournissent des implémentations de deux interfaces : les classes DocumentFilter et NavigationFilter :
  1. Un DocumentFilter s'applique à des implémentations de Document(s) (PlainDocument qui hérite de la classe abstraite AbstractDocument) et vous permet d'intercepter des commandes d'édition et de les modifier comme vous le souhaitez.
  2. Un NavigationFilter peut s'appliquer à des JTextComponent pour contrôler le mouvement du curseur (comme dans un champ de formatage de masque). Partie non-traitée parce que rarement utile.

La classe DocumentFilter

Il est possible d'implémenter ses propres AbstractFormatter pour les utiliser avec JFormattedTextField, et, plus généralement, il est possible d'hériter de la classe DocumentFilter pour contrôler l'édition des documents, dans n'importe quel type de composant texte (qui héritent donc de JTextComponent). Nous pouvons par exemple créer un DocumentFilter qui permet de n'insérer que les caractères numériques.

Un DocumentFilter fournit donc un moyen de contrôler ou d'organiser les entrées utilisateur en bas niveau, élément par élément. Dans le prochain paragraphe, nous aborderons la validation à haut niveau, permettant de vérifier que les données sont correctes une fois saisie.

Pour mémoire, dans l'architecture Modèle-Vue-Contrôleur, le contrôleur traduit les événements de saisie en commandes qui modifient le document sous-jacent du champ de texte, c'est-à-dire la chaîne de texte stockée dans un objet PlainDocument (qui hérite de la classe de base AbstractDocument et qui implémente l'interface Document). Par exemple, lorsque le contrôleur traite une commande "insert string". La chaîne à insérer peut être un caractère unique ou le contenu du tampon. Un filtre de document interceptera cette commande et modifiera la chaîne ou annulera l'insertion.

javax.swing.text.DocumentFilter
void insertString(DocumentFilter.FilterBypass bypass, int offset, String texte, AttributeSet attributs)
Cette méthode est appelée avant l'insertion d'une chaîne dans un Document. Vous pouvez remplacer la méthode et modifier le comportement de la chaîne. Vous pouvez désactiver l'insertion en appelant pas super.insertString() ou en appelant les méthodes bypass pour modifier le document sans filtrage.
- bypass : un objet qui permet d'exécuter des commandes de modification qui contourne le filtre.
- offset : le décalage auquel insérer le texte.
- texte : les caractères à insérer.
- attributs : les attributs de mise en forme du texte inséré.
void remove(DocumentFilter.FilterBypass bypass, int offset, int longueur)
Cette méthode est appelée avant de supprimer une partie d'un document. Récupère le document en appelant bypass.getDocument() pour analyser l'effet de la suppression.
- bypass : un objet qui permet d'exécuter des commandes de modification qui contourne le filtre.
- offset : le décalage de la partie à supprimer.
- longueur : la longueur de la partie à supprimer.
void replace(DocumentFilter.FilterBypass bypass, int offset, int longueur, String texte, AttributeSet attribut)
Cette méthode est appelée avant de remplacer une partie d'un document par une nouvelle chaîne. Vous pouvez remplacer la méthode et modifier le comportement de la chaîne. Vous pouvez désactiver l'insertion en appelant pas super.replace() ou en appelant les méthodes bypass pour modifier le document sans filtrage.
- bypass : un objet qui permet d'exécuter des commandes de modification qui contourne le filtre.
- offset : le décalage auquel insérer le texte.
- longueur : la longueur de la partie à remplacer.
- texte : les caractères à insérer.
- attributs : les attributs de mise en forme du texte inséré.
Exemples d'utilisation

Nous allons traiter deux cas de figure. D'une part, en filtrant un simple JTextField. D'autre part, en filtrant un JFormattedTextField.

  1. Le premier exemple propose un filtre sur un JTextField classique en faisant en sorte que le texte saisie s'affiche automatiquement en lettre capitale :

    package filtres;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.text.*;
    
    public class Filtre extends JFrame {
       private JTextField saisie = new JTextField(15);
    
       public Filtre() {
          super("Saisie du nom");
          AbstractDocument document = (AbstractDocument) saisie.getDocument();
          document.setDocumentFilter(new Majuscule());
          add(new JLabel("Nom :"));
          add(saisie);
          setLayout(new FlowLayout());
          pack();
          setDefaultCloseOperation(EXIT_ON_CLOSE);
          setVisible(true);
       }
    
       class Majuscule extends DocumentFilter {
          @Override
          public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
             super.insertString(fb, offset, string.toUpperCase(), attr);
          }
          @Override
          public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
             super.replace(fb, offset, length, text.toUpperCase(), attrs);
          }     
       }
       
       public static void main(String[] args) { new  Filtre(); }
    }
        

    Dans notre exemple, notez que tous les Document(s) n'ont pas de méthode setDocumentFilter(). Au lieu de cela, nous sommes obligé de transtyper notre document en un AbstractDocument (ou éventuellement en un PlainDocument). Seules les implémentations de document qui utilisent AbstractDocument (comme PlainDocument) acceptent les filtres.

  2. Le deuxième exemple propose un filtre, cette fois-ci, sur un JFormattedTextField, qui permet de saisir uniquement des valeurs numériques. Si l'utilisateur tape des caractères autres que des chiffres, ces caractères ne sont tout simplement pas affichés au moment de la saisie :

    package filtres;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.*;
    import java.text.*;
    import javax.swing.text.*;
    
    public class Filtre extends JFrame {
       private JFormattedTextField saisie = new JFormattedTextField(new FormatNombre());
    
       public Filtre() {
          add(new JLabel("Votre âge :"));
          saisie.setColumns(5);
          add(saisie);
          setLayout(new FlowLayout());
          pack();
          setDefaultCloseOperation(EXIT_ON_CLOSE);
          setVisible(true);
       }
    
       private class FormatNombre extends InternationalFormatter {
          private Nombre nombre = new Nombre();
          public FormatNombre() { super(NumberFormat.getIntegerInstance()); }
          @Override
          protected DocumentFilter getDocumentFilter() { return nombre; }
       }
       
       private class Nombre extends DocumentFilter {
          @Override
          public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {         
             super.insertString(fb, offset, nouvelle(string), attr);
          }
          @Override
          public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
             super.replace(fb, offset, length, nouvelle(text), attrs);
          }     
          private String nouvelle(String ancienne) {
             StringBuilder chaîne = new StringBuilder(ancienne);
             for (int i = chaîne.length()-1; i >=0; i--) 
                if (!Character.isDigit(chaîne.charAt(i))) chaîne.deleteCharAt(i);  
             return chaîne.toString();
          }
       }
       
       public static void main(String[] args) { new  Filtre(); }
    }
    

    Cette fois-ci, nous devons remplacer la méthode getDocumentFilter(), qui permet de spécifier le filtre désiré, d'une classe "formatter", puis transmettre l'objet de cette classe "formatter" au JFormattedTextField. Le problème, c'est que lorsque vous faites cela, vous ne pouvez plus spécifier, en même temps, le type de format désiré. Heureusement, il existe la classe InternationalFormatter qui hérite de DefaultFormatter et qui possède un constructeur de type Format qui permet donc de choisir, en plus du filtre, le type de formatage désiré.

  3. Il est tout à fait possible dans ce contexte de placer la classe Nombre à l'intérieur de la classe FormatNombre :
    ...
    
       private class FormatNombre extends InternationalFormatter {
          private Nombre nombre = new Nombre();
    
          public FormatNombre() { super(NumberFormat.getIntegerInstance()); }
          @Override
          protected DocumentFilter getDocumentFilter() { return nombre; }  
       
          private class Nombre extends DocumentFilter {
            @Override
             public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {         
                super.insertString(fb, offset, nouvelle(string), attr);
            }
            @Override
             public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
                super.replace(fb, offset, length, nouvelle(text), attrs);
             }     
             private String nouvelle(String ancienne) {
               StringBuilder chaîne = new StringBuilder(ancienne);
               for (int i = chaîne.length()-1; i >=0; i--) 
                  if (!Character.isDigit(chaîne.charAt(i))) chaîne.deleteCharAt(i);  
               return chaîne.toString();
            }
        }   
    ...

    Dans ce cas de figure, nous aurions pu, tout à fait prendre de nouveau un JTextField à la place d'un JFormattedTextField et nous nous serions retrouvé alors dans la même situation que le code précédent. Mais lorsque vous devez développer des formats particuliers comme nous l'avons fait avec la classe DefaultFormatter, cela vaut le coup de prendre à la place la classe InternationalFormatter (puisqu'elle hérite de la première) et de proposer alors le filtre désiré.

 

Choix du chapitre HTML et RTF en prime - JEditorPane

La plupart des interfaces utilisateurs n'utiliserons que deux classes filles de JTextComponent : JTextField et JTextArea, que nous venons d'étudier. Elles ne représentent pourtant que la partie visible de l'iceberg. Swing propose des possibilités de texte sophistiquées par l'intermédiaire de deux autres classes filles de JTextComponent : JEditorPane et JTextPane (qui hérite de JEditorPane). Ce chapitre va nous permettre de connaître plus précisément la classe JEditorPane. JTextPane sera traitée dans le chapitre suivant.

Nous avons déjà évoqué que la classe abstraite JTextComponent est en réalité un éditeur très puissant. C'est le composant JEditorPane qui exprime le plus tout son potentiel. Effectivement, JEditorPane permet l'affichage et l'édition de texte complexe formaté comme des documents HTML et RTF, en conjonction avec les classes des paquetages javax.swing.text.html et javax.swing.text.rtf.

La possibilité d'afficher si facilement du texte formaté est une fonctionnalité extrêmement puissante. Par exemple, l'affichage de documents HTML dans une application simplifie l'ajout d'une aide en ligne basée sur une version HTML du manuel d'utilisateur. De plus le texte formaté procure à une application un moyen professionnel d'afficher sa sortie à un utilisateur.

Les possibilités de JEditorPane

La classe JEditorPane permet d'éditer des contenus de natures différentes. Ce composant utilise une implémentation de la classe abstraite EditorKit pour réaliser cela. Par défaut, trois types de contenu sont connus :

  1. text/plain : du texte géré par DefaultEditorKit. Le texte est affiché avec retours à la ligne, quand une ligne dépasse la ligne physique du JEditorPane, et sans coupure de mot.
  2. text/html : du texte géré par HTMLEditorKit, en HTML 3.2.
  3. text/rtf : du texte RTF (Rich Text Format) géré par RTFEditorKit.

Pour charger du texte dans le JEditorPane, nous peuvons utiliser une des méthodes suivantes :

  1. La méthode setText(String s) : dans ce cas, le EditorKit courant est utilisé, et la chaîne de caractères doit contenir du texte du type géré par le EditorKit courant.
  2. La méthode read(Reader r) : dans ce cas, si le contenu de r est de type HTML, les références relatives ne seront résolues que si le tag <base> est utilisé, ou si la propriété Base du HTMLDocument est affectée. L'EditorKit utilisé est l'EditorKit courant.
  3. La méthode setPage(URL url) : dans ce cas, le type du contenu est déterminé par le contenu de l'url, et l'EditorKit adéquat est alors chargé.

Affichage d'une page HTML

Comme HTML est devenu universel, nous nous focaliserons sur l'affichage de documents HTML avec JEditorPane. Il existe plusieurs façons différentes de faire afficher un document HTML :

  1. Si le document désiré est disponible sur le réseau, la manière la plus simple consiste à passer un objet java.net.URL à la méthode setPage() de la classe JEditorPane. setPage() détermine le type de données du document et, en supposant qu'il s'agit d'un document HTML, le charge et l'affiche :

    JEditorPane éditeur = new JEditorPane(); // éditeur vierge
    ...
    éditeur.setPage(new URL("http://www.unsite.fr")); // navigation vers une nouvelle page

  2. Vous pouvez également proposer cette même URL au moment de la construction du JEditorPane, l'effet sera le même :

    JEditorPane éditeur = new JEditorPane(new URL("http://www.unsite.fr")); // proposer une page HTML au départ

  3. Si le document à afficher se trouve dans un fichier local ou est disponible depuis un type de InputStream, nous pouvons l'afficher en passant le flux approprié à la méthode read() de JEditorPane. Le second argument de cette méthode doit être null :

    InputStream fichier = new FileInputStream("fichier.html");
    éditeur.read(fichier, null);

  4. Une autre façon possible d'afficher du texte dans un JEditorPane consiste à passer le texte par la méthode setText() que nous connaissons bien. Avant cela, il faut cependant indiquer à l'éditeur à quel type il doit s'attendre au moyen de la méthode setContentType() :

    éditeur.setContentType("text/html");
    éditeur.setText("<h1>Bienvenue...</h1>");

    L'appel de setText() peut être particulièrement utile quand l'application génère du HTML à la volée et désire employer un JEditorPane pour afficher une sortie formatée à l'utilisateur.

Attention : nous ne pouvons pas prétendre concurencer les navigateurs actuels. JEditorPane est certe capable d'afficher des pages Web, mais il ne faut pas qu'elles soient trop sophistiquées avec prise en compte de styles particuliers, et avec des plugins à télécharger. Restez modeste sur vos pages Web à consulter.

Gestion de la navigation

Il existe un nouveau type d'événement lié à la sélection des liens hypertexte : HyperlinkEvent. Les sous-types de cet événement sont déclenchés lorsque la souris pénètre (ENTERED), sort (EXITED) ou clique (ACTIVATED) sur un lien hypertexte. En l'associant aux possibiltés HTML de JEditorPane, il est très facile de fabriquer un navigateur simple. Lorsque vous devez gérer cet événement, vous devez implémenter l'interface écouteur HyperLinkListener. L'action associée est alors représentée par la méthode hyperlinkUpdate(). Enfin, vous devez préciser à votre JEditorPane qu'il est source de l'événement au moyen de la méthode addHyperlinkListener().

package navigateur;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.ArrayList;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;

public class Navigateur extends JFrame implements ActionListener, HyperlinkListener {
   private JEditorPane éditeur = new JEditorPane();
   private JTextField saisieURL = new JTextField("http://");
   private JToolBar barre = new JToolBar();
   private JButton back = new JButton("<<");   
   private ArrayList<String> historique = new ArrayList<String>();
   
   public Navigateur() {
      super("Navigateur");
      back.setFocusPainted(false);
      back.setPreferredSize(new Dimension(30, 24));
      back.addActionListener(this);
      barre.add(back);
      barre.add(new JLabel(" Adresse : "));
      saisieURL.addActionListener(this);
      barre.add(saisieURL);
      add(barre, BorderLayout.NORTH);
      éditeur.setEditable(false);
      éditeur.addHyperlinkListener(this);
      add(new JScrollPane(éditeur));
      setSize(600, 500);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void ouvrirURL(String adresse) {
      try {
         URL url = new URL(adresse);
         éditeur.setPage(url);
         saisieURL.setText(url.toExternalForm());
         historique.add(adresse);
      } 
      catch (Exception ex) {
         setTitle("Impossible d'ouvrir la page");
      }
   }   

   public void actionPerformed(ActionEvent e) {
      if (e.getSource()==back) {
         historique.remove(historique.size()-1);
         ouvrirURL(historique.get(historique.size()-1));
      }
      else  ouvrirURL(e.getActionCommand());
   }

   public void hyperlinkUpdate(HyperlinkEvent e) {
      HyperlinkEvent.EventType type = e.getEventType();
      if (type == HyperlinkEvent.EventType.ACTIVATED) ouvrirURL(e.getURL().toExternalForm());
   }
   
   public static void main(String[] args) { new  Navigateur(); } 
}

Informations sur une page HTML

Une page HTML est représentée dans un modèle de type HTMLDocument. Voici quelques exemples qui nous permettent de comprendre comment utiliser les classes annexes pour récupérer les informations désirées.

Le titre de la page

(String)monDocHTML.getProperty(Document.TitleProperty);

Les liens de la page
HTMLDocument.Iterator it = monDocHTML.getIterator(HTML.Tag.A);
while(it.isValid()){ 
   SimpleAttributeSet s = (SimpleAttributeSet)it.getAttributes(); 
   String lien = (String)s.getAttribute(HTML.Attribute.HREF); 
   int deb = it.getStartOffset(); 
   int fin = it.getEndOffset(); 
   String v = monDocHTML.getText(deb, fin-deb+1); 
   it.next();
}
Les textes en gras
it = monDocHTML.getIterator(HTML.Tag.B);
while(it.isValid()){ 
   int deb = it.getStartOffset();
   int fin = it.getEndOffset(); 
   String v = monDocHTML.getText(deb, fin-deb+1);
   it.next();
}
Les textes en italique
it = monDocHTML.getIterator(HTML.Tag.I);
while(it.isValid()){
   int deb = it.getStartOffset();
   int fin = it.getEndOffset(); 
   String v = monDocHTML.getText(deb, fin-deb+1);
   it.next();
}
Les images
it = monDocHTML.getIterator(HTML.Tag.IMG);
while(it.isValid()){ 
   RunElement s = (RunElement)it.getAttributes();
   String source = (String)s.getAttribute(HTML.Attribute.SRC);
   String alt    = (String)s.getAttribute(HTML.Attribute.ALT);
   //le fichier : source, et le texte alternatif : alt 
   it.next();
}

Editeur de texte avec gestion de formats différents

Nous l'avons évoqué en préambule, JEditorPane est capable de traiter des documents RTF, avec donc la gestion des styles, comme la police, la couleur du texte, etc. Je pense qu'il est préférable, dans ce cas là, de prendre directement la classe JTextPane, qui hérite de JEditorPane, et qui propose en plus de nouvelles fonctionnalités intéressantes. JTextPane est vraiment spécialisée sur les éditeurs de textes formatés et complexes que nous allons étudier dans le chapitre qui suit.

 

Choix du chapitre Editeur de texte formaté - JTextPane

Swing propose une dernière classe fille de JTextComponent capable de réaliser tout ce que nous souhaitons dans un éditeur classique, comme le choix de la police, la couleur du texte, pouvoir mettre en italique, etc. Les composants texte de base, JTextField et JTextArea, sont limités à une seule police dans un seul style. JTextPane, classe fille de JEditorPane, sait quand à elle afficher plusieurs polices et plusieurs styles dans un même composant. Elle gère également un curseur, la mise en évidence, l'incoporation d'image, ainsi que d'autres fonctionnalités élaborées.

Le composant JTextPane ajoute les fonctionnalités suivantes à JEditorPane :

  1. Possibilité de définition d'une hiérarchie de styles : addStyle(String nom, Style parent)
  2. Possibilité d'ajout de composants Swing dans le JTextPane : insertComponent(Component composant)
  3. Possibilité d'ajout d'images : insertIcon(Icon icône)
  4. Possibilité de spécifier les attributs, comme la taille de la fonte et le style, qui s'appliquent aux caractères individuels : setCharacterAttributes(AttributeSet attribut, boolean remplacer)

    Cette méthode met à jour les attributs du texte sélectionné courant ou, s'il n'y a pas de sélection, spécifie les attributs à appliquer au texte inséré plus tard. L'argument booléen remplacer indique si ces attributs doivent remplacer les anciens ou doivent s'y ajouter.
  5. Possibilité de spécifier les attributs, comme les marges et la justification, sur un paragraphe entier : setParagraphAttributes(AttributeSet attribut, boolean remplacer)
javax.swing.JTextPane
MutableAttributeSet getInputAttributes()
Restitue l'ensemble des attributs constituant le texte du document.
Style getLogicalStyle()
void setLogicalStyle(Style style)
Restituer ou attribuer un style particulier dans l'ensemble des styles définis.
AttributeSet getParagraphAttributes()
void
setParagraphAttributes(AttributeSet attribut, boolean remplacer)
Restitue ou spécifie les attributs, comme les marges et la justification, sur un paragraphe entier.
void setCharacterAttributes(AttributeSet attribut, boolean remplacer)
spécifie les attributs, comme la taille de la fonte et le style, qui s'appliquent aux caractères individuels. Cette méthode met à jour les attributs du texte sélectionné courant ou, s'il n'y a pas de sélection, spécifie les attributs à appliquer au texte inséré plus tard. L'argument booléen remplacer indique si ces attributs doivent remplacer les anciens ou doivent s'y ajouter.
StyledDocument getStyledDocument()
void
setStyledDocument(StyledDocument document)
Récupère ou propose le document capable de maîtriser tous les styles prévus.
Style getStyle(String nom)
Style addStyle(String nom, Style parent)
void removeStyle(String nom)
Récupère, propose un nouveau style à ceux existants suivant une hiérarchie, ou enlève un des styles de la hiérarchie.
void insertComponent(Component composant)
void insertIcon(Icon image)
Ajoute un composant quelconque ou une image.
void replaceSelection(String nom)
Change le texte sélectionné par le nouveau passé en argument. Cette méthode fait également partie de la classe JEditorPane.

JTextPane est un composant qui affiche et édite un texte formaté sur plusieurs lignes. Combiné à une interface graphique qui permet à l'utilisateur de sélectionner les fontes, les couleurs, le style des paragraphes, il offre des fonctionnalités non négligeables de traitement de texte pour une application Java. JTextPane fonctionne avec des documents qui implémentent l'interface javax.swing.text.StyledDocument (qui hérite de l'interface Document), généralement un objet de la classe javax.swing.text.DefaultStyleDocument.

La structure d'un document

JTextPane ne définit pas lui-même des méthodes d'insertion de texte formaté dans le document. Pour cela, il faut travailler impérativement et directement avec le StyleDocument correspondant.

Revenons très brièvement sur la structure de ces différents types de documents ;

Texte plat

Un Document, comme la classe PlainDocument, est un conteneur qui contient du texte, et sert de modèle aux composants Swing permettant d'éditer du texte. Dans ce cadre, le texte est une séquence de caractères unicode, dont le premier caractère est à l'indice 0.

Les méthodes suivantes de Document permettent d'obtenir tout ou partie du texte :

  1. int getLength() : retourne la longueur (nombre de caractères) du texte.
  2. String getText(int d, int l) : retourne le texte entre à partir de d, et de longueur l.
  3. void getText(int d, int l, Segment s) : retourne une partie d'un segment de texte passé en argument.
Texte structuré

Le document de type StyleDocument contient du texte qui possède une structure, par exemple, un livre composé de chapîtres, eux mêmes composés de paragraphes, etc. L'unité de base de la structure est un Element (interface), qui possède un ensemble d'attributs. Les éléments sont de nature différente suivant que nous avons affaire à un texte RTF (comme la classe DefaultStyleDocument) : section, paragraph, content, etc. ou à un texte HTML (comme la classe HTMLDocument) : html, body, p, content, etc.

Les méthodes suivantes permettent d'accéder aux éléments d'un document :

  1. Element getDefaulRootElement() : retourne l'élément racine.
  2. Element[] getRootElements() : retourne les éléments qui dépendent de la racine.
javax.swing.text.Element

Cette interface définit les méthodes requises pour les objets désirant faire partie de l'arborescence des éléments d'un objet Document. Un objet Element doit également conserver la trace de son parent et de ses enfants. Il doit encore connaître sa position et celle de ses enfants à l'intérieur de la séquence linéaire de caractères qui composent le Document. Enfin, un Element doit pouvoir renvoyer l'ensemble d'attributs qui lui ont été appliqués :

AttributeSet getAttributes()
retourne les attributs de l'élément.
Document getDocument()
retourne le document associé à l'élément.
Element getElement(int index)
retourne l'élément fils correspondant à l'index spécifié en argument.
int getElementCount()
retourne le nombre d'éléments fils contenu dans cet élément.
int getElementIndex(int offset)
renvoie l'index de l'élément enfant qui contient la position spécifiée.
int getEndOffset()
renvoie la position de la fin de l'élément.
String getName()
retourne le nom de l'élément.
Element getParentElement()
renvoie le parent de l'élément.
int getStartOffset()
renvoie la position de début de l'élément..
boolean isLeaf()
détermine si cet élément est un élément feuille, c'est-à-dire s'il représente le niveau le plus profond de l'arborescence. Si cet élément comporte un ou plusieurs éléments, la méthode retourne false.
Petite application qui analyse la structure d'une page Web
package analyseur;

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

public class Analyseur extends JFrame {
   private JTextPane éditeur = new JTextPane();
   private JTextArea code = new JTextArea();

   public Analyseur() throws IOException {
      super("Analyse");
      éditeur.setContentType("text/html");
      InputStream fichier = new FileInputStream("index.html"); 
      éditeur.read(fichier, null);
      analyse();
      add(éditeur, BorderLayout.NORTH);
      add(code);
      setSize(400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void analyse() {
      Document document = éditeur.getDocument();
      code.append("Longueur du document : "+document.getLength()+" caractères.\n");
      parcours(document.getDefaultRootElement());
   }
   
   private void parcours(Element élément) {
      for (int i = 0; i < élément.getElementCount(); ++i) {
         code.append(élément.getName()+'('+élément.getStartOffset()+", "+élément.getEndOffset()+")\n");
         if (!élément.isLeaf()) parcours(élément.getElement(i));  
      }
   }
   
   public static void main(String[] args) throws IOException { new  Analyseur(); }
}
javax.swing.text.AttributeSet

Cette interface définit les méthodes de base requises pour un ensemble d'attributs. Elle établit une correspondance entre des noms d'attributs (NameAttribute), ou des clefs, et des valeurs d'attributs (ResolveAttribute). Ces clefs et ces valeurs peuvent être des objets quelconques. La classe StyleConstants définit un certain nombre de clefs couramment utilisées. L'interface AttributSet définit quatre interfaces internes (CharacterAttribute, ColorAttribute, FontAttribute, ParagraphAttribute). Ces interfaces vides servent de marqueurs et doivent être implémentées par un objet clef pour en spécifier la catégorie générale.

Un AttributeSet peut avoir un autre AttributeSet pour parent. Quand nous recherchons une valeur avec getAttribute(), nous commençons par les correspondances locales. En cas d'échec, la recherche continue (de manière récursive) sur l'AttributeSet parent. L'ensemble d'attributs parent est lui-même stocké comme un attribut, avec la clef définie par la constante ResolveAttribute. On appelle getResolveParent() pour connaître l'AttributSet parent. Les méthodes isDefined() et getAttributeNames() n'opèrent que sur les correspondances locales et n'utilisent pas l'AttributeSet parent.

NameAttribute
ResolveAttribute
constantes publiques qui spécifies le nom et la valeur de l'attribut.
CharacterAttribute
ColorAttribute
FontAttribute
ParagraphAttribute
interfaces qui servent de marqueur qui permettent de spécifier la catégorie générale.
boolean constainsAttribute(Object nom, Object valeur)
boolean constainsAttributes(AttributeSet attributs)
détermine si le ou les attributs sont présents.
AttributeSet copyAttributes()
copie et retourne l'ensemble des attributs.
Object getAttribute(Object clef)
retourne la valeur de l'attribut correspondant à la clef spécifiée en argument.
int getAttributeCount()
renvoie le nombre d'attributs.
Enumeration getAttributeNames()
retourne l'ensemble des attributs.
AttributeSet getResolveParent()
renvoie l'attribut parent.
boolean isDefined(Object nom)
détermine si l'attribut, dont le nom est spécifié en argument, existe.
boolean isEqual(AttributeSet attribut)
teste l'égalité entre deux attributs.
Retour sur l'application précédente qui visualise en plus les attributs des balises
private void analyse() {
   Document document = éditeur.getDocument();
   code.append("Longueur du document : "+document.getLength()+" caractères.\n");
   parcours(document.getDefaultRootElement());
}
   
private void parcours(Element élément) {
   for (int i = 0; i < élément.getElementCount(); ++i) {
      code.append(élément.getName()+'('+élément.getStartOffset()+", "+élément.getEndOffset()+')'+attributs(élément)+"\n");
      if (!élément.isLeaf()) parcours(élément.getElement(i));  
   }
}

private String attributs(Element élément) {
   StringBuilder chaîne = new StringBuilder();
   AttributeSet attribut = élément.getAttributes();
   Enumeration liste = attribut.getAttributeNames();
   while(liste.hasMoreElements()){
      Object clef = liste.nextElement();
      chaîne.append(" - "+clef+ " = " +attribut.getAttribute(clef));
   }
   return chaîne.toString();
}









Modification du texte et de ses attributs (Mutation)

Pour ajouter du texte formaté avec un style particulier, il faudra en plus de ce qui a été fait ci-dessus, passer par le biais d’une instance de Document, c'est-à-dire un DefaultStyledDocument, et de la méthode insertString(). Il est également possible de supprimer une partie de document avec la méthode remove(), de remplacer les styles d'une partie de document replace(), etc.

JTextPane est un composant qui affiche et édite un texte formaté sur plusieurs lignes. Combiné à une interface graphique qui permet à l'utilisateur de sélectionner les fontes, les couleurs, le style des paragraphes, etc. Il offre des fonctionnalités non négligeables de traitement de texte pour une application Java. JTextPane fonctionne avec des documents qui implémentent l'interface javax.swing.text.StyledDocument (qui hérite de l'interface Document), généralement un objet de la classe javax.swing.text.DefaultStyleDocument.





Voici ci-dessous l'ensemble des méthodes de la classe DefaultStyleDocument, dont certaines ont déjà été utilisées :

javax.text.swing.DefaultStyleDocument
void addDocumentListener(DocumentListener écouteur)
void removeDocumentListener(DocumentListener écouteur)
DocumentListener[] getDocumentListeners()
<T extends EventListener> T[] getListeners(Class<T> listenerType)
Place ou enlève un écouteur spécifique de document.
void addUndoableEditListener(UndoableEditListener écouteur)
void removeUndoableEditListener(UndoableEditListener écouteur)
UndoableEditListener[] getUndoableEditListeners()
void insertUpdate(AbstractDocument.DefaultDocumentEvent chng, AttributeSet attr)
void postRemoveUpdate(AbstractDocument.DefaultDocumentEvent chng)
void removeUpdate(AbstractDocument.DefaultDocumentEvent chng)
Gestion des fonctions copier, couper, coller, undo, etc.
Style addStyle(String nom, Style parent)
Style getLogicalStyle(int position)
Style getStyle(String nom)
Enumeration<?> getStyleNames()
void removeStyle(String nom)
void setLogicalStyle(int position, Style style)
Gestion des styles dans le document.
Position createPosition(int offset)
Position getStartPosition()
Position getEndPosition()
Gestion des positions dans le texte.
Element getDefaultRootElement()
Element[] getRootElements()
Element getParagraphElement(int position)
Element getCharacterElement(int position)
Gestion des parties du document.
Color getBackground(AttributeSet attribut)
Color getForeground(AttributeSet attribut)
Font getFont(AttributeSet attribut)
void setCharacterAttributes(int offset, int longueur, AttributeSet attribut, boolean remplacer)
void setParagraphAttributes(int offset, int longueur, AttributeSet attribut, boolean remplacer)
Gestion des attributs.
int getLength()
Renvoie la longueur des données.
String getText(int offset, int longueur)
void getText(int offset, int longueur, Segment txt)
Récupération du texte.
void dump(PrintStream out)
Renvoie toutes les données vers le flux concerné.
void insertString(int offset, String chaîne, AttributeSet attributs)
void remove(int offset, int longueur)
void replace(int offset, int longueur, String texte, AttributeSet attributs)
Ajout, suppresion, et remplacement de texte avec les attributs concernés.
Object getProperty(Object clef)
void putProperty(Object clef, Object valeur)
Dictionary<Object,Object> getDocumentProperties()
void setDocumentProperties(Dictionary<Object,Object> x)
Placement et restitution de propriétés particulières au document.

Comment ajouter un nouveau paragraphe avec des styles personnalisés définis dans le JTextPane

Pour introduire du texte avec des styles personnalisés dans votre éditeur, nous devons travailler avec plusieurs éléments. Il existe toutefois plusieurs approches possibles pour créer des styles. Dans un premier temps, je vais mettre en place mes styles directement à partir de la classe JTextPane (nous verrons ultérieurement une autre approche qui consiste à créer de toute pièce un document, de type DefaultStyledDocument, avec la création séparée d'un ensemble de styles pour les appliquer ensuite, globalement, au JTextPane). Voici donc la procédure à suivre :

  1. Nous devons commencer par définir l'ensemble des styles désirés directement dans le composant JTextPane au travers de la méthode addStyle(). Une fois que ces styles sont définis, il sera ensuite possible de choisir celui qui convient pour le paragraphe (ou la sélection) désiré.
  2. Les styles sont définis au travers de la classe StyleConstants qui possèdent des méthodes statiques spécialisées à la fois sur des styles propres aux caractères, comme la mise en gras, l'italique, la couleur, etc. mais aussi sur des styles plus adaptés à la mise en forme du paragraphe, comme les justifications, les marges (hautes, basse, gauche et droite) et même l'indentation de la première ligne.
  3. Vous introduiser ensuite votre texte avec l'ensemble des paragraphes requis au travers de la méthode insertString() de la classe représentant un StyledDocument. Lorsque vous utilisez cette méthode, vous spécifiez en même temps le style souhaité.
  4. Il est possible, à ce moment là, de prévoir un style de paragraphe au travers de la méthode setParagraphAttributes().
Définition des styles hiérarchisés
  1. L'idéal est de pouvoir mettre en place des styles hiérarchisés en commençant par le style par défaut déjà présent dans le JTextPane. Effectivement, il est tout à fait possible, comme pour d'autres composants graphiques, de spécifier une police - au moyen de setFont() - une couleur de fond - au moyen de setBackground() - etc. Nous obtenons le style par défaut de la façon suivante :

    JTextPane texte = new JTextPane();
    Style
    racine = texte.getStyle("default");

  2. Nous pouvons modifier l’une des caractéristiques du style par défaut ainsi :

    // Pour modifier le type de police
    StyleConstants.setFontFamily(racine, "SansSerif");
    // Pour modifier la taille de la police
    StyleConstants.setFontSize(racine, 16);
    // Pour changer la justification par défaut
    StyleConstants.setAlignment(racine, StyleConstants.ALIGN_JUSTIFIED);

  3. Les styles peuvent être directement définis à partir du style racine, en proposant une hiérarchie, grâce à la méthode addStyle() de JTextPane :

    Style nouveau = texte.addStyle("Nom du style", racine); // C'est vous qui choisissez l'intitulé du style.

  4. En voici quelques exemples :

    Style x = texte.addStyle("Italique", racine);
    StyleConstants.setItalic(x, true);
    Style y = texte.addStyle("Gras", racine);
    StyleConstants.setBold(y, true);
    Style composant = texte.addStyle("Validation", racine);
    StyleConstants.setComponent(composant, new JCheckBox("bouton"));
    Style justification = texte.addStyle("Justification", racine);
    StyleConstants.setAlignment(justification, StyleConstants.RIGHT);

  5. Une fois que l'ensemble des styles préconisés sont définis, il est possible d'attribuer un style particulier à l'ensemble du JTextPane, grâce à la méthode setLogicalStyle() :

    texte.setLogicalStyle(texte.getStyle("Italique")); // Attention, ce style va être opérationnel sur tout le document

  6. De la même façon, nous pouvons définir au JTextPane comment vont être structurés l'ensemble des paragraphes grâce à la méthode setParagraphAttributes() :

    texte.setParagraphAttributes(texte.getStyle("Justification"), true); // Attention, tous les paragraphes seront suivant ce même style

Ajouter des paragraphes, avec prise en compte des styles, au fur et à mesure dans l'éditeur
StyleDocument document = texte.getStyledDocument();
try {
document.insertString(document.getLength(), "Première ligne de texte.\n" , racine);
document.insertString(document.getLength(), "Seconde ligne de texte (en gras).\n", texte.getStyle("Gras"));
// Nous pouvons même ajouter des composants
document.insertString(document.getLength(), " ", texte.getStyle("Validation")); }
catch (BadLocationException e) { ... }
Nous pouvons modifier les attribut d'un texte, ou d'un paragraphe
String t = ...; // le texte de remplacement
int d = ...; // à partir de d
int f = ...; // jusqu'à f
Style attributs = texte.addStyle("attributs", texte.getStyle("default"));
StyleConstants.setFontFamily(attributs, "Courier");
StyleConstants.setFontSize(attributs, 20);
StyleConstants.setBackground(attributs, Color.YELLOW);
StyleConstants.setForeground(attributs, Color.RED);
StyleConstants.setAlignment(attributs, 1);
try {
// On remplace le texte, avec les nouveaux attributs
document.replace(d, f-d, t, attributs);
// Les attributs du paragraphe englobant sont modifiés
document.setParagraphAttributes(d, f-d, attributs, true); } catch (BadLocationException e1) { ... }

Remarquez bien qu'il est possible d'attribuer un style, soit directement à partir d'un objet Style, ici l'objet attributs ou plus haut l'objet racine, soit en récupérant celui qui a été introduit dans le JTextPane, au moyen de la méthode getStyle(), comme plus haut avec : texte.getStyle("Validation") ou texte.getStyle("Gras").

La classe StyleConstants

La classe StyleConstants contient les méthodes de classe qui permettent de définir un Style en modifiant le style des caractères, du paragraphe, ou les tabulations. Cette classe définit un certain nombre de clefs d'attributs standards (d'où le terme StyleConstants) pour des attributs de caractère et de paragraphe couramment employés. Elle définit également plusieurs méthodes statiques de commodité qui utilisent ces attributs à partir d'un AttributeSet ou pour modifier la valeur d'un attribut dans un MutableAttributeSet.

Généralement, le type de la valeur associé à une clef d'attribut se déduit du contexte. Les signatures des méthodes statiques getXxx() et setXxx() rendent la valeur explicite. La valeur associée à clef Alignment doit être l'une des quatre constantes ALIGN_ définie par la classe. Les valeurs des longueurs aux attributs comme LeftIndent et LineSpacing doivent être des float exprimés en point d'impression (il y a 72 points d'impression par pouce pour un écran).

StyleConstants définit quatre sous-classes internes (CharacterConstants, ColorConstants, FontConstants, ParagraphConstants), chacune d'entre-elles implémentant une interface de marquage différente servant à regrouper les clefs d'attributs en grandes catégories. Ces classes internes définissent également des constantes de clefs, mais celles-ci sont aussi directement accessibles par la classe StyleConstants.

Style des caractères dans la classe javax.swing.text.StyleConstants
String getFontFamily(AttributeSet attribut)
void setFontFamily(AttributeSet attribut, String police)
Retourne ou modifie le nom de la police de l'attribut proposé.
int getFontSize(AttributeSet attribut)
void setFontSize(AttributeSet attribut, int taille)
Retourne ou modifie la taille de la police de l'attribut proposé.
Color getBackground(AttributeSet attribut)
Color getForeground(AttributeSet attribut)
void setBackground(AttribuetSet attribut, Color couleur)
void setBackground(AttribuetSet attribut, Color couleur)
Retourne ou propose la couleur du fond et du texte.
boolean isItalic(AttributeSet attribut)
void setItalic(AttributeSet attribut, boolean b)
boolean isBold(AttributeSet attribut)
void setBold(AttributeSet attribut, boolean b)
boolean isStrikeThrough(AttributeSet attribut)
void setStrikeThrough(AttributeSet attribut, boolean b)
boolean isUnderline(AttributeSet attribut)
void setUnderline(AttributeSet attribut, boolean b)
Détermine ou spécifie le style de l'attribut, respectivement en italique, en gras, surligné ou souligné.
boolean isSuperscript(AttributeSet attribut)
void setSuperscript(AttributeSet attribut, boolean b)
boolean isSubscript(AttributeSet attribut)
void setSubscript(AttributeSet attribut, boolean b)
Détermine ou spécifie la mise en exposant et un indice.
Style des paragraphes dans la classe javax.swing.text.StyleConstants


int getAlignment(AttributeSet attribut)
void setAlignment(AttributeSet attribut, int al)
Retourne ou modifie la valeur de l'alignement de l'attribut. Cet alignement peut être :
StyleConstants.ALIGN_CENTER
StyleConstants.ALIGN_LEFT
StyleConstants.ALIGN_RIGHT
StyleConstants.ALIGN_JUSTIFIED

float getSpaceAbove(AttributeSet attribut)
void setSpaceAbove(AttributeSet attribut, float f)
float getSpaceBelow(AttributeSet attribut)
void setSpaceBelow(AttributeSet attribut, float f)
float getLeftIndent(AttributeSet attribut)
void setLeftIndent(AttributeSet attribut, float f)
float getRightIndent(AttributeSet attribut)
void setRightIndent(AttributeSet attribut, float f)
Ajuste ou retourne les alignements de paragarphe, respectivement au dessus, en dessous, à gauche ou à droite.
float getFirstLineIndent(AttributeSet attribut)
void setFirstLineIndent(AttributeSet attribut, float f)
Indentation de la przmière ligne.
float getLineSpacing(AttributeSet attribut)
void setLineSpacing(AttributeSet attribut, float f)
Espacement entre les lignes.
La gestion des tabulations dans la classe javax.swing.text.StyleConstants
TabSet getTabSet(AttributeSet attribut)
void setTabSet(AttributeSet attribut, TabSet ts)
Retourne ou modifie l'ensemble des tabulations de l'attribut.
Placement (ou restitution) des composants et des images dans la classe javax.swing.text.StyleConstants
Component getComponent(AttributeSet attribut)
void setComponent(MutableAttributeSet attribut, Composant composant)
Restitue ou place un nouveau composant, généralement graphique (boutons, labels, tables, etc.)
Icon getIcon(AttributeSet attribut)
void setIcon(MutableAttributeSet attribut, Composant image)
Restitue ou place une nouvelle image.
Exemple d'application
package styles;

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

public class StylesDansDocument extends JFrame {
   private JTextPane texte = new JTextPane();
   private JLabel bienvenue = new JLabel("Bienvenue...");

   public StylesDansDocument() {
      super("Gestion des styles");
      add(new JScrollPane(texte));
      bienvenue.setBorder(new EtchedBorder());
      bienvenue.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 24));
      formater();
      placerContenu();
      setSize(400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private void formater() {     
      texte.setFont(new Font("Verdana", Font.BOLD, 16));
      texte.setBackground(Color.YELLOW);
      Style défaut = texte.getStyle("default");  
      // Modification du style par défaut pour que tous les paragraphes
// prennent en compte cette justification
StyleConstants.setAlignment(défaut, StyleConstants.ALIGN_JUSTIFIED); StyleConstants.setSpaceAbove(défaut, 13.0F); StyleConstants.setLeftIndent(défaut, 7.0F); StyleConstants.setSpaceBelow(défaut, 20.0F); StyleConstants.setRightIndent(défaut, 7.0F); StyleConstants.setLineSpacing(défaut, -0.2F); texte.setParagraphAttributes(défaut, true); // Style retrait qui hérite du style par défaut et qui propose un retrait supplémentaire. Style retrait = texte.addStyle("retrait", défaut); StyleConstants.setLeftIndent(retrait, 25.0F); StyleConstants.setRightIndent(retrait, 25.0F); // Style de caractères particuliers Style rouge = texte.addStyle("rouge", défaut); StyleConstants.setForeground(rouge, Color.RED); StyleConstants.setItalic(rouge, true); StyleConstants.setUnderline(rouge, true); // Régler un style générique Style centré = texte.addStyle("centré", défaut); StyleConstants.setAlignment(centré, StyleConstants.ALIGN_CENTER); // Pouvoir placer un composant Swing quelconque Style composant = texte.addStyle("composant", centré); StyleConstants.setComponent(composant, bienvenue); // Pouvoir placer une image Style image = texte.addStyle("image", centré); StyleConstants.setIcon(image, new ImageIcon("mésange bleue.jpg")); } private void placerContenu() { String message = "Bienvenue à tout le monde, et bonjour pour ceux qui sont à distance, au loin.\n"; ajoutParagraphe(message, "default", null); ajoutParagraphe(message, "rouge", "retrait"); ajoutParagraphe(message, "default", null); ajoutParagraphe("\n", "composant", "centré"); ajoutParagraphe(message, "rouge", null); ajoutParagraphe("\n", "image", "centré"); } private void ajoutParagraphe(String message, String caractères, String paragraphe) { try { StyledDocument doc = texte.getStyledDocument(); int début = doc.getLength(); doc.insertString(début, message, texte.getStyle(caractères)); if (paragraphe!=null) doc.setParagraphAttributes(début, message.length(), texte.getStyle(paragraphe), false); } catch (BadLocationException ex) { } } public static void main(String[] args) { new StylesDansDocument(); } }

Les styles

Un Style est un ensemble d'attributs à appliquer à une partie d'un document. L'ensemble des styles d'un document est un StyleContext. Un style peut s'appliquer à une suite de caractères ou à un paragraphe. Un style sur une suite de caractères écrase le style du paragraphe où se trouve la suite de caractères. Les styles sont hiérarchisés, et la racine de la hiérarchie est le style par défaut.

Un style est encapsulé dans un objet d’une classe SimpleAttributeSet implémentant l’interface Style. Les styles peuvent former une arborescence. A partir d’un parent unique, nous définissons les autres éléments de l’arborescence, ceci au travers de la classe StyleContext. La racine est donc le style par défaut. La classe StyleContext comporte un ensemble de SimpleAttributeSet.


javax.swing.text.MutableAttributeSet

Cette interface étend AttributeSet pour ajouter des méthodes permettant de modifier l'ensemble des attributs et des parents.

void addAttribute(Object nom, Object valeur)
void addAttributes(AttributeSet attributs)
rajoute un attribut ou un ensemble d'attributs.
void removeAttribute(Object nom)
void removeAttributes(AttributeSet attributs)
void removeAttributes(Enumeration noms)
enlève un attribut ou un ensemble d'attributs.
void setResolveParent(AttributeSet parent)
propose l'attribut parent spécifié en argument.
javax.swing.text.Style

Cette interface étend MutableAttributSet en ajoutant à la fois une méthode de commodité servant à obtenir le nom de l'ensemble des attributs et des méthodes d'enregistrement de ChangeListener. Un objet Style est généralement utilisé pour représenter un ensemble nommé d'attributs. Le nom du style est souvent stocké en tant qu'attribut. Comme un Style est une sorte de MutableAttributSet, les objets qui l'utilisent peuvent vouloir savoir quand les attributs du Style changent. Ils se voient notifier par un ChangeEvent quand les attributs sont ajoutés au Style ou quand ils en sont retirés, au moyen respectivement des méthodes addChangeListener() et removeChangeListener().

javax.swing.text.SimpleAttributeSet

La classe SimpleAttributSet, comme son nom l'indique, permet de gérer un attribut sur lequel il est possible de placer différents styles que nous souhaitons soumettre à une partie de notre document.

SimpleAttributeSet()
SimpleAttributeSet(AttributeSet attribut)
Constructeurs publics. Le deuxième permet de construire un ensemble de style à partie d'un attribut déjà constitué. Il faut d'abord bien remplir ce dernier pour que cela soit bien efficace.
static final AttributeSet EMPTY
Constante publique qui identifie le style par défaut du document.
boolean isEmpty()
Y-a-t-il des attributs.
boolean constainsAttribute(Object nom, object valeur)
boolean constainsAttributes(AttributeSet attribut)
boolean isDefined(Object nom)
boolean isEqual(AttributeSet attribut)
Contrôle la présence d'un ou plusieurs attributs.
AttributeSet copyAttributes(Object nom, object valeur)
Object getAttribute(Object nom)
AttributeSet getResolveParent()
Restitue le ou les attributs.
int getAttributeCount()
Retourne le nombre d'attributs.
Enumeration getAttributeNames()
Renvoie le nom de l'ensemble des attributs.
AttributeSet addAttribute(Object nom, Object valeur)
AttributeSet addAttributes( AttributeSet attribut)
Ajouter un ou plusieurs attributs.
AttributeSet removeAttribute(Object nom)
AttributeSet removeAttributes(AttributeSet attributs)
AttributeSet removeAttributes(Enumeration noms)
Suppression d'un attribut ou d'un ensemble d'attributs.
void setResolveParent(AttributeSet attribut)
Spécifie l'attribut parent.
javax.swing.text.StyleContext

La classe StyleContext reprend les caractéristiques de la classe SimpleAttributeSet en proposant en plus une hiérarchisation des attributs. Ainsi, vous pouvez appliquer un groupe de styles particuliers à l'ensemble du document comme le choix de la police, de la justification, de la couleur de fond et ensuite, tout en concervant ces caractéristiques, vous pouvez proposer des styles personnalisés pour un paragraphe, comme la couleur du texte, la mise en gras et en italique, etc.

Cette classe est à la fois une collection et une fabrique d'objets Style. Elle est implémentée de manière à mettre en cache et à réutiliser des ensembles d'attributs communs. Nous utilisons la méthode addStyle() pour créer un nouvel objet Style et l'ajouter à la collection. Nous employons les méthodes de l'objet Style renvoyé pour spécifier les attributs du Style.

  1. getStyle() : recherche un style par son nom.
  2. removeStyle() : retire un style de la collection.
  3. getDefaultStyleContext() : méthode statique qui renvoie un objet StyleContext par défaut convenant à une utilisation partagée par plusieurs documents.
  4. getFont() : permet d'accéder aux instances de Font partagées. StyleContext comprend un cache simple de Font.
static final String DEFAULT_STYLE
Constante publique qui identifie le style par défaut du document.
static final StyleContext getDefaultStyleContext()
Méthode statique qui renvoie un objet StyleContext par défaut convenant à une utilisation partagée par plusieurs documents.
static Object getStaticAttribute(Object clef)
static Object getStaticAttributeKey(Object clef)
Méthodes statiques qui retournent l'attribut suivant la clef spécifiée en argument.
void addChangeListener(ChangeListener écouteur)
void removeChangeListener(ChangeListener écouteur)
Placement ou suppression d'un écouteur de changement de style.
void addStyle(String nom, Style parent)
void removeStyle(String nom)
Ajoute ou supprime un style dans la collection.
Color getBackground(AttributeSet attribut)
Font getFont(AttributeSet attribut)
Font getFont(String famille, int style, int taille)
FontMetrics getFontMetrics(Font fonte)
Color getForeground(AttributeSet attribut)
Renvoie des styles particuliers de l'attribut concerné.
void readAttributes(ObjectInputStream fichier, MutableAttributeSet attributs)
void writeAttributes(ObjectOutputStream fichier, AttributeSet attributs)
static void writeAttributeSet(ObjectOutputStream fichier, AttributeSet attribut)
Récupérer ou conserver des attributs (styles) sur fichiers (ou flux quelconques comme en réseau).
AttributeSet addAttribute(AttributeSet ancien, Object nom, Object valeur)
AttributeSet addAttributes(AttributeSet ancien, AttributeSet attribut)
AttributeSet removeAttribute(AttributeSet ancien, Object nom)
AttributeSet removeAttributes(AttributeSet ancien, AttributeSet attribut)
AttributeSet removeAttributes(AttributeSet ancien, Enumeration noms)
Ajout ou suppression d'un attribut ou d'un ensemble d'attributs.
AttributeSet getEmptySet()
void reclaim(AttributeSet attribut)
Récupérer un attribut particulier ou un attribut vide de tout style.

Fabriquer un document stylisé avant de le soumettre au JTextPane

Récemment, nous avons mis en oeuvre une application qui permettait de mettre en oeuvre des styles. Les styles ont été fabriqués et recencés directement dans le JTextPane. Nous allons reprendre cette application avec une autre approche. Nous devons d'abord créer des styles à part entière indépendemment du JTextPane, tout en respectant la hiérarchisation, au travers de la classe StyleContext. Nous créons également un document stylisé, de type DefaultStyledDocument, toujours indépendemment du JTextPane. Nous plaçons ensuite les différents paragraphes souhaités dans ce document vierge ainsi que les composants nécessaires tout en appliquant les styles désirés. Une fois que ce document est correctement rempli, nous le soumettons enfin au JTextPane.

package styles;

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

public class StylesDansDocument extends JFrame {
   private JTextPane texte = new JTextPane();
   private JLabel bienvenue = new JLabel("Bienvenue...");
   private StyleContext styles;
   private DefaultStyledDocument document;

   public StylesDansDocument() {
      super("Gestion des styles");
      add(new JScrollPane(texte));
      bienvenue.setBorder(new EtchedBorder());
      bienvenue.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 24));   
      définirStyles();    
      construireDocument();
      texte.setStyledDocument(document);
      texte.setBackground(Color.YELLOW);
      setSize(400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private void définirStyles() {     
      styles = StyleContext.getDefaultStyleContext();
      Style défaut = styles.getStyle(StyleContext.DEFAULT_STYLE);  
      // Modification du style par défaut pour que tous les paragraphes prennent 
           en compte cette justification
      Style racine = styles.addStyle("racine", défaut);
      StyleConstants.setFontFamily(racine, "Verdana");
      StyleConstants.setBold(racine, true);
      StyleConstants.setFontSize(racine, 16);
      StyleConstants.setAlignment(racine, StyleConstants.ALIGN_JUSTIFIED);
      StyleConstants.setSpaceAbove(racine, 13.0F);
      StyleConstants.setLeftIndent(racine, 7.0F);
      StyleConstants.setSpaceBelow(racine, 20.0F);
      StyleConstants.setRightIndent(racine, 7.0F);
      StyleConstants.setLineSpacing(racine, -0.2F);       
      // Style retrait qui hérite du style par défaut et qui propose un retrait supplémentaire.
      Style retrait = styles.addStyle("retrait", racine);
      StyleConstants.setLeftIndent(retrait, 25.0F);
      StyleConstants.setRightIndent(retrait, 25.0F);
      // Style de caractères particuliers
      Style rouge = styles.addStyle("rouge", racine);
      StyleConstants.setForeground(rouge, Color.RED);
      StyleConstants.setItalic(rouge, true);
      StyleConstants.setUnderline(rouge, true);
      // Régler un style générique
      Style centré = styles.addStyle("centré", racine);
      StyleConstants.setAlignment(centré, StyleConstants.ALIGN_CENTER);      
      // Pouvoir placer un composant Swing quelconque
      Style composant = styles.addStyle("composant", centré);
      StyleConstants.setComponent(composant, bienvenue);
      // Pouvoir placer une image
      Style image = styles.addStyle("image", centré);
      StyleConstants.setIcon(image, new ImageIcon("mésange bleue.jpg"));
   }   
   
   private void construireDocument() {
      document = new DefaultStyledDocument();
      String message = "Bienvenue à tout le monde, et bonjour pour ceux qui sont à distance, au loin.\n";
      ajoutParagraphe(message, "racine", "racine");        
      ajoutParagraphe(message, "rouge", "retrait");
      ajoutParagraphe(message, "racine", "racine");    
      ajoutParagraphe("\n", "composant", "centré"); 
      ajoutParagraphe(message, "rouge", null); 
      ajoutParagraphe("\n", "image", "centré");            
   }
   
   private void ajoutParagraphe(String message, String caractères, String paragraphe) {
      try {
         int début = document.getLength();
         document.insertString(début, message, styles.getStyle(caractères));
         if (paragraphe!=null) document.setParagraphAttributes(début, message.length(), styles.getStyle(paragraphe), false);            
      } 
      catch (BadLocationException ex) {  }
   }

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

Fabrication de styles à partir de la classe SimpleAttributeSet

Lorsque vous utilisez la classe SimpleAttributeSet, vous fabriquer un jeu de style séparément. Toutefois, lorsqu'un style est constitué et que vous désirez le prendre en compte dans un autre style, il suffit de le placer en arguement dans le constructeur de ce dernier. Nous obtenons ainsi une hiérarchisation. Il faut noter qu'il ne s'agit pas en réalité d'un hiérarchie de style, comme dans le cas de la classe StyleContext, mais d'une simple duplication, si d'ailleurs le premier attribut a bien été mis en place au préalable. Voici comment procéder en reprenant l'application précédente.

package styles;

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

public class StylesDansDocument extends JFrame {
   private JTextPane texte = new JTextPane();
   private JLabel bienvenue = new JLabel("Bienvenue...");
   private SimpleAttributeSet défaut;
   private SimpleAttributeSet retrait;
   private SimpleAttributeSet rouge;
   private SimpleAttributeSet centré;
   private SimpleAttributeSet composant;
   private SimpleAttributeSet image;
   private DefaultStyledDocument document;

   public StylesDansDocument() {
      super("Gestion des styles");
      add(new JScrollPane(texte));
      bienvenue.setBorder(new EtchedBorder());
      bienvenue.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 24));   
      définirStyles();    
      construireDocument();
      texte.setStyledDocument(document);
      texte.setBackground(Color.YELLOW);
      setSize(400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private void définirStyles() {     
      // Style par défaut pour que tous les paragraphes prennent en compte cette justification 
      défaut = new SimpleAttributeSet();
      StyleConstants.setFontFamily(défaut, "Verdana");
      StyleConstants.setBold(défaut, true);
      StyleConstants.setFontSize(défaut, 16);
      StyleConstants.setAlignment(défaut, StyleConstants.ALIGN_JUSTIFIED);
      StyleConstants.setSpaceAbove(défaut, 13.0F);
      StyleConstants.setLeftIndent(défaut, 7.0F);
      StyleConstants.setSpaceBelow(défaut, 20.0F);
      StyleConstants.setRightIndent(défaut, 7.0F);
      StyleConstants.setLineSpacing(défaut, -0.2F);       
      // Style retrait qui hérite du style par défaut et qui propose un retrait supplémentaire.
      retrait = new SimpleAttributeSet(défaut);
      StyleConstants.setLeftIndent(retrait, 25.0F);
      StyleConstants.setRightIndent(retrait, 25.0F);
      // Style de caractères particuliers
      rouge = new SimpleAttributeSet(défaut);
      StyleConstants.setForeground(rouge, Color.RED);
      StyleConstants.setItalic(rouge, true);
      StyleConstants.setUnderline(rouge, true);
      // Régler un style générique
      centré = new SimpleAttributeSet(défaut);
      StyleConstants.setAlignment(centré, StyleConstants.ALIGN_CENTER);      
      // Pouvoir placer un composant Swing quelconque
      composant = new SimpleAttributeSet(centré);
      StyleConstants.setComponent(composant, bienvenue);
      // Pouvoir placer une image
      image = new SimpleAttributeSet(centré);
      StyleConstants.setIcon(image, new ImageIcon("mésange bleue.jpg"));
   }   
   
   private void construireDocument() {
      document = new DefaultStyledDocument();
      String message = "Bienvenue à tout le monde, et bonjour pour ceux qui sont à distance, au loin.\n";
      ajoutParagraphe(message, défaut, défaut);        
      ajoutParagraphe(message, rouge, retrait);
      ajoutParagraphe(message, défaut, défaut);    
      ajoutParagraphe("\n", composant, centré); 
      ajoutParagraphe(message, rouge, null); 
      ajoutParagraphe("\n", image, centré);            
   }
   
   private void ajoutParagraphe(String message, AttributeSet caractères, AttributeSet paragraphe) {
      try {
         int début = document.getLength();
         document.insertString(début, message, caractères);
         if (paragraphe!=null) document.setParagraphAttributes(début, message.length(), paragraphe, false);            
      } 
      catch (BadLocationException ex) {  }
   }

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

Réalisation d'un tout petit éditeur

Le sujet est très vaste. Il y aurait encore beaucoup de chose à dire. Je vous propose pour conclure de réaliser un tout petit éditeur qui va nous permettre de comprendre un petit peu les mécanismes que nous avons évoqués tout au long de ce chapitre. Il est sans prétention, et son fonctionnement est des plus modeste. Il permet toutefois de comprendre un petit peu l'interraction entre le texte saisie et les boutons de formatage. Notamment, lorsque vous déplacez le curseur du texte, vous remarquez que les boutons de gras et d'italique s'enfoncent si le texte comportent l'un de ces formatages particuliers.

Codage de l'éditeur
package styles;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Enumeration;
import javax.swing.event.*;
import javax.swing.text.*;

public class StylesDansDocument extends JFrame implements ActionListener, CaretListener {
   private JTextPane texte = new JTextPane();
   private JToolBar barre = new JToolBar();
   private JToggleButton boutonItalique = new JToggleButton("<html><i>Italique</i></html>");
   private JToggleButton boutonGras = new JToggleButton("<html>Gras</html>");
   private JButton boutonCouleur = new JButton("<html>Couleur</html>");
   private Color couleur = Color.BLUE;

   public StylesDansDocument() {
      super("Editeur");
      add(new JScrollPane(texte));
      boutonItalique.addActionListener(this);
      barre.add(boutonItalique);
      boutonGras.addActionListener(this);
      barre.add(boutonGras);
      boutonCouleur.setForeground(couleur);
      boutonCouleur.addActionListener(this);
      barre.add(boutonCouleur);
      add(barre, BorderLayout.NORTH);
      texte.addCaretListener(this);
      formater();
      setSize(400, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private void formater() {     
      texte.setFont(new Font("Verdana", Font.PLAIN, 13));
      texte.setBackground(Color.YELLOW);
      Style défaut = texte.getStyle("default");  
      StyleConstants.setAlignment(défaut, StyleConstants.ALIGN_JUSTIFIED);
      StyleConstants.setSpaceAbove(défaut, 3.0F);
      StyleConstants.setLeftIndent(défaut, 5.0F);
      StyleConstants.setSpaceBelow(défaut, 7.0F);
      StyleConstants.setRightIndent(défaut, 5.0F);
      StyleConstants.setLineSpacing(défaut, -0.2F);
      StyleConstants.setForeground(défaut, couleur);
      texte.setParagraphAttributes(défaut, true);  
   }   
   
   public void actionPerformed(ActionEvent e) {
      SimpleAttributeSet attribut = new SimpleAttributeSet();
      if (e.getSource()==boutonItalique) StyleConstants.setItalic(attribut, boutonItalique.isSelected()); 
      if (e.getSource()==boutonGras) StyleConstants.setBold(attribut, boutonGras.isSelected()); 
      if (e.getSource()==boutonCouleur) {
         couleur = JColorChooser.showDialog(this, "Couleur du texte", couleur); 
         StyleConstants.setForeground(attribut, couleur);
         boutonCouleur.setForeground(couleur);
      }
      texte.setCharacterAttributes(attribut, false);
      texte.repaint();
      texte.requestFocus();
   }
   
   public void caretUpdate(CaretEvent e) {
      AttributeSet attribut= texte.getCharacterAttributes();
      Enumeration liste = attribut.getAttributeNames();
      ArrayList<String> noms = new ArrayList<String>();
      while (liste.hasMoreElements()) noms.add(liste.nextElement().toString());
      boutonItalique.setSelected(noms.contains("italic"));
      boutonGras.setSelected(noms.contains("bold"));      
      barre.revalidate();
   }
   
   public static void main(String[] args) { new  StylesDansDocument(); }   
}