Dans cette partie nous allons nous concentrer sur l'étude de tout ce qui représente une saisie de texte ou de valeurs formatée comme les valeurs numériques ou les dates, jusqu'à la mise en place d'éditeurs relativement sophistiqués.
Nous entendons par saisie, toutes les valeurs introduites à l'aide du clavier. Il existe d'autres possibilités qui permettent de récupérer des valeur numériques notamment à partir de composants graphiques comme les curseurs ou les slidders. Ce sera l'objet d'une autre étude.
Le composant le plus complexe de Swing est le composant JTextComponent, qui est un éditeur puissant. Il fait partie du paquetage javax.swing.text. Vous ne pouvez pas construire vous-même un objet JTextComponent, car il s'agit d'une classe abstraite. En réalité, vous employez plus souvent l'une des sous-classe suivantes :
La possibilité d'afficher si facilement du texte formaté est une fonctionnalité extrêmement puissante. Par exemple, l'affichage de document HTML dans une application simplifie l'ajout d'une aide en ligne basée sur une version HTML du manuel de l'utilisateur. De plus, le texte formaté procure à une application, un moyen professionnel d'afficher sa sortie à un utilisateur.
Pour étudier les modifications apportées à des composants texte, il est nécessaire de connaître la façon dont ils implémentent l'architecture MVC (Modèle-Vue-Contrôleur). Les composants texte vont nous permettre de bien distinguer les partie M et VC. Le modèle de composants textes est un objet baptisé Document.
Document est une interface. Cette interface est implémentée par la classe abstraite AbstractDocument. Cette classe abstraite est héritée par la classe fille PlainDocument. Ainsi, lorsque nous faisons référence un élément de type Document, il s'agira en interne, d'un objet PlainDocument.
Lorsque nous ajoutons ou supprimons du texte d'un JTextField ou d'un JTextArea, le Document correspondant est modifié. C'est le composant lui-même, et non les composants visuels, qui génère les événements texte lorsqu'un changement se produit. Par conséquent, pour être informé des modification de JTextArea, nous nous enregistrons auprès du Document concerné, et non auprès du composant JTextArea proprement dit :
JTextArea saisie = new JTextArea(); Document texte = saisie.getDocument();
texte.addDocumentListener(écouteur);
En outre, les composants JTextField génèrent un ActionEvent à chaque fois que l'utilisateur appuie sur la touche Entrée dans le champ. Pour recevoir ces événements, implémentez l'interface ActionListener et appelez addActionListener() pour effectuer l'enregistrement de votre écouteur.
JTextComponent délivrent un certain nombre de fonctionnalités communes à toutes ces classes filles. En voici quelques unes qui me paraissent intéressantes (liste non exhaustive) :
saisie.setMargin(new Insets(haut, gauche, bas, droit));
JTextField permet à l'utilisateur d'entrer et d'éditer une ligne unique de texte simple. Nous pouvons lire et écrire le texte avec les méthode getText() et setText() héritées de la super-classe JTextComponent. Nous pouvons également sollliciter les méthodes propres à la classe JTextField :
JTextField déclenche un ActionEvent aux écouteurs de type ActionListener quand l'utilisateur tape sur la touche "Entrée" du clavier. Nous pouvons éventuellement spécifier le texte de la commande d'action envoyée avec la'événement ActionEvent en appelant la méthode setActionCommand().
Plusieurs constructeurs sont à votre disposition pour créer des champs de texte. Le premier est le constructeur par défaut. La largeur du champ de texte dépend alors du gestionnaire utilisé. Dans la plupart des cas, vous devrez toutefois spécifier ultérieurement le nombre de colonne attendu au moyen de la méthode setColumns().
JTextField saisie = new JTextField("Introduisez votre texte", 20);Ce code crée un champ de texte et l'initialise avec la chaîne Introduisez votre texte. Le second paramètre en définit la largeur. Dans notre exemple, la largeur est de 20 colonnes.
Malheureusement, une colonne est une unité de mesure assez imprécise. Elle représente la largeur attendue d'un caractère dans la police employée pour le texte. Si vous prévoyez des entrées utilisateurs d'au plus n caractères, vous êtes supposé indiquer n en tant que largeur de colonne. Dans la pratique, cette mesure ne donne pas de très bons résultats et vous devrez ajouter 1 ou 2 à la longueur d'entrée maximale prévue.
Gardez aussi à l'esprit que le nombre de colonnes n'est qu'une suggestion pour AWT pour indiquer une taille de préférence. Si le gestionnaire de mise en forme a besoin d'agrandir ou de réduire le champ de texte, il peut ajuster la taille.
La largeur de colonne que vous définissez dans le constructeur de la classe JTextField ne limite pas pour autant le nombre de caractères que l'utilisateur peut taper. Il peut introduire des chaînes plus longues ; toutefois, la vue de l'entrée défile lorsque le texte dépasse la longueur du champ souhaitée.
JTextField saisie = new JTextField(20);
Les méthodes suivantes permettent de travailler avec le texte de la zone de saisie :
String texte = saisie.getText().trim();
Les touches de raccourci (Ctrl-C, Ctrl-V ou Ctrl-X), qui permettent de faire du copier-coller en passant par le presse papier, sont tout à fait opérationnels avec ce composant. Cette fonctionnalité est en réalité héritée de la super-classe JTextComponent.
Vous avez ci-dessous un exemple de codage qui permet de comprendre l'utilisation de ces champs de texte avec les diverses méthodes et phases de construction que nous venons de voir. J'en profite pour prendre quelques fonctionnalités de la super-classe JTextComponent :
package saisie; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Champ extends JFrame implements ActionListener { private JLabel intituléNom = new JLabel("Nom :"); private Saisie nom = new Saisie("Votre nom"); private JLabel intituléPrénom = new JLabel("Prénom :"); private Saisie prénom = new Saisie("Votre prénom"); private JButton validation = new JButton("Valider"); private Saisie résultat = new Saisie("Effectuer votre saisie"); public Champ() { super("Saisie des références"); résultat.setEditable(false); gestionDisposition(); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); setVisible(true); validation.addActionListener(this); } private class Saisie extends JTextField { public Saisie(String texte) { super(texte, 20); setFont(new Font("Verdana", Font.BOLD, 12)); setMargin(new Insets(0, 3, 0, 0)); } } private void gestionDisposition() { GroupLayout groupe = new GroupLayout(getContentPane()); getContentPane().setLayout(groupe); groupe.setAutoCreateContainerGaps(true); groupe.setAutoCreateGaps(true); GroupLayout.ParallelGroup horzGroupe = groupe.createParallelGroup(); GroupLayout.SequentialGroup vertGroupe = groupe.createSequentialGroup(); horzGroupe.addComponent(intituléNom).addComponent(nom).addComponent(intituléPrénom).addComponent(prénom); horzGroupe.addComponent(validation).addComponent(résultat); vertGroupe.addComponent(intituléNom).addComponent(nom).addComponent(intituléPrénom).addComponent(prénom); vertGroupe.addComponent(validation).addComponent(résultat); groupe.setHorizontalGroup(horzGroupe); groupe.setVerticalGroup(vertGroupe); } public void actionPerformed(ActionEvent e) { résultat.setText(prénom.getText()+' '+nom.getText()); } public static void main(String[] args) { new Champ(); } }
La classe Insets permet de spécifier des marges intérieures près du bord, respectivement en haut, à gauche, en bas et à droite.
.
Nous allons maintenant voir comment assurer le suivi, en temps réel, des modifications dans les champs de texte. Pour cela, nous allons mettre en oeuvre une horloge avec deux champs de texte qui permettent de saisir les heures et les minutes. Dès que le contenu des champs est modifié, l'horloge est mise à l'heure.
Garder la trace de tout changement intervenant dans les champs de texte nécessite des efforts supplémentaires. Tout d'abord sachez que surveiller les frappes du clavier ne suffit pas. Certaines touches, telles que les touches fléchées, ne modifient pas le texte.
Nous l'avons abordé au début de cette étude, le champ de texte Swing est implémenté via une méthode générique : la chaîne que vous voyez dans le champ n'est qu'une manifestation visuelle (la vue) d'une structure de données sous-jacente (le modèle). Bien sûr, pour un simple champ de texte, il n'existe pas de différence importante entre ces deux concepts. La vue est une chaîne affichée et le modèle est un objet chaîne. Toutefois, c'est cette même architecture qui est utilisée dans les composants d'édition plus avancés pour présenter du texte formaté avec des polices, des paragraphes et d'autres attributs, représentés en interne par une structure de données plus complexe.
saisie.getDocument().addDocumentListener(écouteur);
Lorsque le texte a changé, l'une des méthodes DocumentListener suivante est appelée :
void insertUpdate(DocumentEvent événement); void removeUpdate(DocumentEvent événement);
void changedUpdate(DocumentEvent événement);
Les deux premières méthodes sont appelées lorsque des caractères ont été insérés ou supprimés. La troisième méthode n'est pas appelée pour les champs de texte. Pour des documents plus comlexes, elle sera appelée pour certains types de modification, tel qu'un changement de mise en forme. Malheureusement, il n'existe pas de méthode de rappel unique pour vous indiquer que le texte a été modifié - généralement, vous ne vous préoccupez pas de la façon dont il a changé.
Il n'existe pas non plus de classe Adapter. Ainsi, l'écouteur de document doit implémenter les trois méthodes.
.
package horloge; import java.awt.*; import java.awt.event.*; import java.text.DateFormat; import java.util.Calendar; import javax.swing.*; import javax.swing.border.EtchedBorder; import javax.swing.event.*; public class Champ extends JFrame implements ActionListener { private Timer minuteur = new Timer(1000, this); private JLabel horloge = new JLabel(); private JPanel panneau = new JPanel(); private Saisie heure; private Saisie minutes; private Calendar date = Calendar.getInstance(); public Champ() { super("Horloge"); setBounds(100, 100, 220, 100); setDefaultCloseOperation(EXIT_ON_CLOSE); horloge.setFont(new Font("Arial", Font.BOLD+Font.ITALIC, 32)); horloge.setHorizontalAlignment(JLabel.CENTER); horloge.setBorder(new EtchedBorder()); add(horloge); minuteur.start(); panneau.add(new JLabel("Heure :")); panneau.add(heure = new Saisie(""+date.get(Calendar.HOUR_OF_DAY))); panneau.add(new JLabel("Minutes :")); panneau.add(minutes = new Saisie(""+date.get(Calendar.MINUTE))); add(panneau, BorderLayout.SOUTH); setResizable(false); setVisible(true); } public void actionPerformed(ActionEvent e) { date.add(Calendar.SECOND, 1); horloge.setText(DateFormat.getTimeInstance(DateFormat.MEDIUM).format(date.getTime())); } private class Saisie extends JTextField implements DocumentListener { public Saisie(String libellé) { super(libellé, 3); setHorizontalAlignment(RIGHT); getDocument().addDocumentListener(this); } public void insertUpdate(DocumentEvent e) { changement(); } public void removeUpdate(DocumentEvent e) { changement(); } public void changedUpdate(DocumentEvent e) { } private void changement() { try { int h = Integer.parseInt(heure.getText().trim()); int m = Integer.parseInt(minutes.getText().trim()); date.set(Calendar.HOUR_OF_DAY, h); date.set(Calendar.MINUTE, m); } catch (NumberFormatException erreur) {} } } public static void main(String[] args) { new Champ(); } }
Ce code ne fonctionnera toutefois pas correctement si l'utilisateur tape une chaîne telle que "deux", qui ne représente pas un chiffre entier, ou s'il laisse le champ de texte vierge. La méthode parseInt() déclenche alors l'exception NumberFormatException qu'il faut capturer. Ici, l'horloge n'est tout simplement pas mis à jour si l'utilisateur n'entre pas un nombre.
Au lieu d'écouter les événements de document, vous pouvez aussi ajouter un écouteur d'action pour un champ de texte. Celui-ci est notifié lorsque l'utilisateur appuie sur la touche Entrée.
private class Saisie extends JTextField implements ActionListener { public Saisie(String libellé) { super(libellé, 3); setHorizontalAlignment(RIGHT); addActionListener(this); } public void actionPerformed(ActionEvent e) { try { int h = Integer.parseInt(heure.getText().trim()); int m = Integer.parseInt(minutes.getText().trim()); date.set(Calendar.HOUR_OF_DAY, h); date.set(Calendar.MINUTE, m); } catch (NumberFormatException erreur) {} } }
Parfois, vous avez besoin de recueillir une entrée d'utilisateur d'une longueur supérieure à une ligne. JTextArea affiche ainsi plusieurs lignes de texte simple non formaté et permet à l'utilisateur d'éditer ce texte. Lorsque vous placez un composant de ce type dans votre programme, un utilisateur peut taper n'importe quel nombre de lignes de texte en utilisant la touche Entrée pour les séparer. Chaque ligne se termine par un caractère de retour de ligne '\n'.
JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacuneoù le paramètre de colonne qui indique le nombre de colonne fonctionne comme auparavant ; vous devez toujours ajouter quelques colonnes (caractères) supplémentaires par précaution.
L'utilisateur n'est pas limité au nombre de lignes et de colonnes ; le texte défilera si l'entrée est supérieure aux valeurs spécifiées. Vous pouvez également modifier le nombre de colonnes et de lignes en utilisant, respectivement, les méthodes setColumns() et setRows(). Les valeurs données n'indiquent qu'une préférence, le gestionnaire de mise en forme peut toujours agrandir ou réduire la zone de texte.
saisie.setLineWrap(true); // sauts de ligne automatiqueCe renvoi à la ligne n'est qu'un effet visuel. Le texte dans le document n'est pas modifié, aucun caractère '\n' n'est inséré.
JTextArea saisie = new JTextArea(8, 40); // 8 lignes de 40 colonnes chacuneLe panneau de défilement gère ensuite la vue de la zone de texte. Des barres de défilement apparaissent automatiquement si le texte entré dépasse la zone d'affichage ; elles disparaissent si, lors d'une suppression, le texte restant tient dans la zone de texte. Le défilement est géré en interne par le panneau de défilement - votre programme n'a pas besoin de traiter les événements de défilement.
JScrollPane ascenceur = new JScrollPane(saisie);
C'est un mécanisme général que vous rencontrerez fréquemment en travaillant avec Swing. Pour ajouter des barres de défilement à un composant, placez-le à l'intérieur d'un panneau de défilement.
Par défaut, JTextField et JTextArea sont éditables ; nous pouvons y écrire et modifier du texte. Ces deux composants peuvent être changés en lecture seule en appelant la méthode setEditable(false).
Tous deux supportent également les sélections. Une sélection est une portion de texte mise en inverse vidéo et pouvant être copié, coupée ou collée dans votre système de fenêtrage. Vous sélectionnez le texte avec la souris ; vous pouvez alors le couper, le copier et le coller dans une autre fenêtre en utilisant des raccourcis clavier. Dans la plupart des systèmes, nous utilisons Ctrl-C pour copier, Ctrl-V pour coller et Ctrl-X pour couper. Il est également possible de gérer ces opérations par programme en utilisant les méthodes cut(), copy() et paste() de JTextComponent.
La sélection de texte courante est renvoyée par getSelectedText(), et vous pouvez définir la sélection en utilisant selectText() avec un indice ou bien selectAll().
Je vous propose, à titre d'exemple, de mettre en oeuvre un éditeur de texte simple du même style que le bloc-note de Windows. J'en profite pour implémenter la plupart des méthodes intéressantes issues de la classe parente JTextComponent, savoir :
package editeur; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.event.*; public class Editeur extends JFrame { private Actions actionNouveau = new Actions("Nouveau", "Tout effacer dans la zone d'édition"); private Actions actionOuvrir = new Actions("Ouvrir", "Ouvrir le fichier texte"); private Actions actionEnregistrer = new Actions("Enregistrer", "Sauvegarder le texte"); private Actions actionCopier = new Actions("Copier", "Copier le texte sélectionné"); private Actions actionCouper = new Actions("Couper", "Couper le texte sélectionné"); private Actions actionColler = new Actions("Coller", "Coller à l'emplacement du curseur"); private JMenuBar menu = new JMenuBar(); private JMenu fichier = new JMenu("Fichier"); private JMenu édition = new JMenu("Edition"); private JPanel panneau = new JPanel(); private JTextField positions = new JTextField(" Lignes : 1 Colonnes : 1"); private JTextField lireSélection = new JTextField(24); private ZoneEdition éditeur = new ZoneEdition(); public Editeur() { super("Nouveau document"); setDefaultCloseOperation(EXIT_ON_CLOSE); actionEnregistrer.setEnabled(false); add(new JScrollPane(éditeur)); positions.setEditable(false); panneau.add(positions); panneau.add(new JLabel(" Sélection :")); lireSélection.setEditable(false); lireSélection.setMargin(new Insets(0, 3, 0, 3)); panneau.add(lireSélection); add(panneau, BorderLayout.SOUTH); menu(); pack(); setVisible(true); } private void menu() { setJMenuBar(menu); menu.add(fichier); fichier.add(actionNouveau); fichier.add(actionOuvrir); fichier.add(actionEnregistrer); menu.add(édition); JMenuItem sélection = new JMenuItem("Tout sélectionner"); édition.add(sélection); sélection.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { éditeur.selectAll(); } }); édition.add(actionCopier); édition.add(actionCouper); édition.add(actionColler); final JCheckBoxMenuItem lectureSeule = new JCheckBoxMenuItem("Lecture seule"); édition.add(lectureSeule); lectureSeule.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { éditeur.setEditable(!lectureSeule.isSelected()); } }); } private class ZoneEdition extends JTextArea implements DocumentListener, CaretListener { public ZoneEdition() { super(15, 36); setMargin(new Insets(3,3,3,3)); // marge intérieure setBackground(Color.YELLOW); setForeground(Color.BLUE); setFont(new Font("Verdana", Font.BOLD, 13)); setSelectedTextColor(Color.YELLOW); // couleur rouge sur le texte sélectionné setSelectionColor(Color.RED); // couleur jaune sur le fond du texte sélectionné setCaretColor(Color.BLUE); // curseur de texte en bleu setTabSize(3); getDocument().addDocumentListener(this); addCaretListener(this); } public void insertUpdate(DocumentEvent e) { actionEnregistrer.setEnabled(true); } public void removeUpdate(DocumentEvent e) { actionEnregistrer.setEnabled(true); } public void changedUpdate(DocumentEvent e) { } public void caretUpdate(CaretEvent e) { int lignes = 1; int colonnes = 1; for (int i=0; i<getCaretPosition(); i++) { if (getText().charAt(i)=='\n') { lignes++; colonnes=1; } else colonnes++; } positions.setText(" Lignes : "+lignes+" Colonnes : "+colonnes); lireSélection.setText(getSelectedText()); panneau.revalidate(); } } private class Actions extends AbstractAction { private String méthode; private JFileChooser boîte = new JFileChooser(); public Actions(String libellé, String description) { super(libellé, new ImageIcon(Editeur.class.getResource(libellé.toLowerCase()+".gif"))); putValue(SHORT_DESCRIPTION, description); putValue(MNEMONIC_KEY, (int)libellé.charAt(0)); méthode = libellé.toLowerCase(); } public void actionPerformed(ActionEvent e) { try { this.getClass().getDeclaredMethod(méthode).invoke(this); } catch (Exception ex) { setTitle("Problème");} } private void nouveau() { setTitle("Nouveau document"); éditeur.setText(""); actionEnregistrer.setEnabled(false); } private void ouvrir() throws IOException { if (boîte.showOpenDialog(Editeur.this)==JFileChooser.APPROVE_OPTION) { setTitle(boîte.getSelectedFile().getName()); éditeur.read(new FileReader(boîte.getSelectedFile()), null); } } private void enregistrer() throws IOException { if (boîte.showSaveDialog(Editeur.this)==JFileChooser.APPROVE_OPTION) { setTitle(boîte.getSelectedFile().getName()); éditeur.write(new FileWriter(boîte.getSelectedFile())); } } private void copier() throws IOException { éditeur.copy(); } private void couper() throws IOException { éditeur.cut(); } private void coller() throws IOException { éditeur.paste(); } } public static void main(String[] args) { new Editeur(); } }
J'aimerais rester quelques instants sur l'architecture Modèle-Vue-Contrôleur. L'exemple que je vous propose ci-dessous montre combien il est facile de partager un même Document par plusieurs composants. En effet, je reprends le projet précédent auquel je rajoute une autre zone de saisie qui est liée à la première puisqu'elles utilisent le même modèle de données. Ainsi, nous pouvons consulter à la fois le début et la fin d'un document grâce à ces deux vues différentes associées à ce même texte.
Nous pouvons agir aussi bien sur une vue que sur l'autre. Ainsi, la sélection peut se faire indifféremment sur la première zone de texte ou sur la deuxième.
deuxième.setDocument(premier.getDocument()); // mise en place d'un document commun pour deux vues différentes
... private JTextField lireSélection = new JTextField(24); private ZoneEdition éditeur = new ZoneEdition(); private ZoneEdition deuxième = new ZoneEdition(); public Editeur() { super("Nouveau document"); setDefaultCloseOperation(EXIT_ON_CLOSE); actionEnregistrer.setEnabled(false); add(new JScrollPane(éditeur), BorderLayout.NORTH); deuxième.setDocument(éditeur.getDocument()); add(new JScrollPane(deuxième)); positions.setEditable(false); ...
Le champ de mot de passe représenté par JPasswordField est un type spécial de champ de texte (sous-classe de JTextField). Il est conçu pour saisir des mots de passe et d'autres donnée sensibles. Pour éviter que des voisins curieux ne puissent s'apercevoir le mot de passe entré par un utilisateur, les caractères tapés ne sont pas affichés. Un caractère d'écho est utilisé à la place, généralement un astérisque (*). Eventuellement, la méthode setEchoChar() permet de choisir le caractère d'écho à faire apparaître au lieu des caractères entrés par l'utilisateur.
Le champ de mot de passe est un autre exemple de la puissance de l'architecture Modèle-Vue-Contrôleur. Il utilise le même modèle qu'un champ de texte standard pour conserver les données, mais sa vue a été modifiée pour n'afficher que des caractères d'écho.
Normalement, getText() permet de récupérer le texte saisi dans le champ de mot de passe, mais cette méthode est devenue désuète. Vous devez plutôt utiliser getPassword(), qui renvoie un tableau de caractères et non un objet String. Les tableaux de caractères sont moins vulnérables que les String face aux programmes renifleurs de mots de passe dans la mémoire. Si cela ne vous concerne pas outre mesure, vous pouvez créer un nouveau String à partir du tableau de caractères. Remarquez que les méthodes des classes de cryptographie de Java acceptent les mots de passe sous forme de tableaux de caractères, non sous forme de chaînes ; il est donc très cohérent de transmettre le résultat d'un appel à getPassword() directement aux méthodes des classes cryptographiques, sans créer le moindre String.
package saisie; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Champ extends JFrame implements ActionListener { private JLabel intituléId = new JLabel("Identifiant :"); private JTextField identifiant = new JTextField(20); private JLabel intituléPasse = new JLabel("Mot de passe :"); private JPasswordField passe = new JPasswordField(); private JButton validation = new JButton("Valider"); public Champ() { super("Saisie des références"); // passe.setEchoChar('#'); gestionDisposition(); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); setVisible(true); validation.addActionListener(this); } private void gestionDisposition() { GroupLayout groupe = new GroupLayout(getContentPane()); getContentPane().setLayout(groupe); groupe.setAutoCreateContainerGaps(true); groupe.setAutoCreateGaps(true); GroupLayout.ParallelGroup horzGroupe = groupe.createParallelGroup(); GroupLayout.SequentialGroup vertGroupe = groupe.createSequentialGroup(); horzGroupe.addComponent(intituléId).addComponent(identifiant).addComponent(intituléPasse).addComponent(passe).addComponent(validation); vertGroupe.addComponent(intituléId).addComponent(identifiant).addComponent(intituléPasse).addComponent(passe).addComponent(validation); groupe.setHorizontalGroup(horzGroupe); groupe.setVerticalGroup(vertGroupe); } public void actionPerformed(ActionEvent e) { setTitle(identifiant.getText()+" : "+String.valueOf(passe.getPassword())); } public static void main(String[] args) { new Champ(); } }
Dans les saisies, nous avons souvent besoin de récupérer des valeurs numériques, une suite de chiffres et non des chaînes de caractères arbitraires. Dans ce cas là, l'utilisateur n'est autorisé à taper que des chiffres de 0 à 9 et un signe moins "-". Si ce signe est utilisé, il doit représenter le premier caractère de la chaîne d'entrée.
En apparence, la validation d'entrée semble simple. Nous pouvons mettre en oeuvre un écouteur de touche pour le champ de texte et bloquer tous les événements des touches qui ne représentent pas un chiffre ou un signe moins. Malheureusement, cette approche simple, bien que recommandée comme méthode de validation d'entrée, ne fonctionne pas bien dans la pratique. Tout d'abord, certaines associations de touches autorisées ne constituent pas obligatoirement une entrée valide, par exemple, - -3 ou 3 - 3.
Plus important encore, il existe d'autres moyens de modifier le texte qui ne font pas appel à la pression d'une touche. Selon le style d'interface implémenté, certaines combinaisons de clavier peuvent servir pour couper, copier ou coller du texte. Pour cette raison, nous devrions aussi nous assurer que l'utilisateur ne colle pas de caractères invalides. Bref, cette tentative de filtrer les frappes du clavier pour valider une entrée commence à devenir complexe.
Heureusement, il existe une classe, JFormattedTextField, qui palie à ce genre de problème. En effet, ce composant offre un support explicite pour éditer des valeurs formatées complexes comme les chiffres, mais également les dates, et des mises en forme plus ésotériques, comme les adresses IP.
JFormattedTextField agit un peu comme JTextField, sauf qu'il accepte dans son constructeur un objet spécifiant le format et gère un type d'objet complexe (comme Date ou Integer) via ses méthodes setValue() et getValue(). L'exemple qui suit montre la construction d'un simple écran avec plusieurs types de champ formatés :
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import java.util.Date; import javax.swing.text.MaskFormatter; public class TexteFormaté extends JFrame implements ActionListener { private SimpleDateFormat formatDate = new SimpleDateFormat("dd/MM/yyyy"); private JFormattedTextField anniversaire = new JFormattedTextField(formatDate); private JFormattedTextField âge = new JFormattedTextField(NumberFormat.getIntegerInstance()); private JFormattedTextField téléphone; private Box groupe = Box.createVerticalBox(); public TexteFormaté() throws ParseException { super("Saisie"); setSize(250, 150); groupe.add(new JLabel("Date anniversaire :")); anniversaire.setValue(new Date()); anniversaire.addActionListener(this); groupe.add(anniversaire); groupe.add(new JLabel("Âge : ")); âge.setValue(new Integer(48)); âge.addActionListener(this); groupe.add(âge); groupe.add(new JLabel("Téléphone :")); téléphone = new JFormattedTextField(new MaskFormatter("0#.##.##.##.##")); téléphone.setValue("04.71.63.55.08"); téléphone.addActionListener(this); groupe.add(téléphone); add(groupe); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { Date date = (Date)anniversaire.getValue(); Number nombre = (Number)this.âge.getValue(); int âge = nombre.intValue(); String téléphone = (String)this.téléphone.getValue(); setTitle(formatDate.format(date)+" : "+âge+" : "+téléphone); } public static void main(String[] args) throws ParseException { new TexteFormaté(); } }
Reportez-vous à l'étude suivante - Formater nombre et date - pour prendre connaissance plus précisément sur ces différents type de formatage.
.
La construction étant faite, vous pouvez définir une valeur valide en utilisant setValue() et récupérer la dernière valeur valide avec getValue(). Pour ce faire, vous devez transtyper la valeur dans le bon type, basé sur le format que vous utilisez. Par exemple, cette commande récupère la date du champ anniversaire :
Date date = (Date)anniversaire.getValue();
Un objet de type JFormattedTextField valide le texte lorsque l'utilisateur essaie de transférer le focus sur un nouveau champ (soit en cliquant en dehors du champ, soit en utilisant la navigation du clavier). Par défaut, JFormattedTextField gère les entrées invalides en retournant tout simplement à la dernière valeur valide.
Nous allons maintenant voir en détail l'ensemble des formats que nous pouvons traiter. Nous allons ainsi reprendre cette étude préliminaire en proposant une analyse plus fine. Commençons par un cas facile : un champ de texte pour la saisie d'un entier.
JFormattedTextField champEntier = new JFormattedTextField(NumberFormat.getIntegerInstance());
NumberFormat.getIntegerInstance() renvoie un objet de mise en forme qui formate les entiers à l'aide des paramètres régionaux. Dans les paramètres français, les virgules servent de séparateurs décimaux (ce qui permet de saisir des valeurs comme 1,72), les espaces servent de séparateur de milliers.JFormattedTextField champEntier = new JFormattedTextField(new Integer(37));
Toutefois, l'autoboxing fonctionne tout à fait correctement depuis la version 5 de java, vous pouvez donc écrire directement :JFormattedTextField champEntier = new JFormattedTextField(37);
champEntier.setColumns(20);
champEntier.setValue(new Integer(37));
Encore une fois, vous pouvez utiliser l'autoboxing :champEntier.setValue(37);
Number nombre = (Number)champEntier.getValue();
int entier = nombre.intValue();
Pour connaître les fonctionnalités des classes enveloppes comme Integer, repportez vous à la rubrique suivante : Classes enveloppes.
.
Le champ de texte mis en forme n'est pas très intéressant tant que vous ne pensez pas à ce qui survient lorsque l'utilisateur saisit des données non autorisées. C'est le sujet de la prochaine section.
Imaginons un utilisateur entrant des données dans un champ de texte. Il tape des informations, puis décide finalement de quitter le champ, par exemple en cliquant sur un autre composant. Le champ de texte perd alors le focus (la focalisation). Le curseur en (I) n'y est plus visible et les frappes sur les touches sont destinées à un autre composant.
Lorsque le champ de texte mis en forme perd le focus, l'élément de mise en forme étudie la chaîne de texte produite par l'utilisateur. S'il sait la convertir en objet, le texte est considéré comme valide, sinon il est signalé non valide. Vous pouvez utiliser la méthode isEditValid() pour vérifier la validité du champ de texte.
Le comportement par défaut en cas de perte de focalisation est appelé "commit or revert" (engager ou retourner). Si la chaîne de texte est valide, elle est engagée (commited). Le formateur la transforme en objet, qui devient la valeur actuelle du champ (c'est-à-dire la valeur de retour de la méthode getValue() vue à la section précédente). La valeur est ensuite retransformée en chaîne, qui devient la chaîne de texte visible dans le champ.
Le formateur d'entier reconnaît par exemple que l'entrée 1729 est valide, il définit la valeur actuelle sur new Long(1729), puis la retransforme en chaîne en insérant un espace pour les milliers : 1 729.
A l'inverse, si la chaîne de texte n'est pas valide, la valeur n'est pas modifiée et le champ de texte retourne à la chaîne représentant l'ancienne valeur.
.
Par exemple, si l'utilisateur entre une valeur erronée, comme x1, l'ancienne valeur est récupérée lorsque le champ de texte pert le focus.
Le formateur d'entier considère une chaîne de texte comme valide si elle commence par un entier. Par exemple, 1729x est une chaîne valide. Elle est transformée en 1729, puis mis en forme (1 729).
JFormattedTextField prend bien sûr en charge d'autres formateurs en plus du formateur d'entier. La classe NumberFormat, je le rappelle, possède les méthodes statiques suivantes :
Reportez-vous à l'étude suivante - Format de nombre personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; public class Conversion extends JFrame implements ActionListener { private JFormattedTextField saisie = new JFormattedTextField(NumberFormat.getCurrencyInstance()); private JFormattedTextField résultat = new JFormattedTextField(new DecimalFormat("#,##0.00 F")); public Conversion() { super("Conversion €uro -> Francs"); saisie.setColumns(25); saisie.setValue(0); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); résultat.setValue(0); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { final double TAUX = 6.55957; double €uro = ((Number)saisie.getValue()).doubleValue(); double franc = €uro * TAUX; résultat.setValue(franc); } public static void main(String[] args) { new Conversion(); } }
Pour éditer les dates et les heures, appelez l'une des méthodes statiques de la classe DateFormat :
Pour la gestion des dates, il est possible de spécifier plus précisément le format désiré. Ainsi, si vous désirez avoir le format :
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.SHORT));
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance()); // ou DateFormat.MEDIUM
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.LONG));
JFormattedTextField date = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.FULL));
Par défaut, le format de date est assez clément. Ainsi, une date non valide comme le 31 février 2007 est transformé pour indiquer la prochaine date valide, à savoir le 3 mars 2007. Attention, ce comportement peut surprendre les utilisateurs ! Dans ce cas, appelez setLenient(false) sur l'objet DateFormat.
Reportez-vous à l'étude suivante - Format de date personnalisé - pour prendre connaissance plus précisément sur ce type de formatage.
.
package format; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import java.util.Date; public class JourSemaine extends JFrame implements ActionListener { private JFormattedTextField saisie = new JFormattedTextField(DateFormat.getDateInstance(DateFormat.SHORT)); private JFormattedTextField résultat = new JFormattedTextField(new SimpleDateFormat("EEEE")); public JourSemaine() { super("Jour de la semaine"); saisie.setColumns(20); saisie.setValue(new Date()); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); résultat.setValue(new Date()); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { résultat.setValue((Date)saisie.getValue()); } public static void main(String[] args) { new JourSemaine(); } }
DefaultFormatter est capable de mettre en forme les objets de toute classe qui disposent d'un constructeur avec un paramètre de chaîne et une méthode toString() correspondante. Par exemple, la classe URL dispose d'un constructeur URL(String) pouvant être utilisé pour construire une URL depuis une chaîne, comme :
URL url = new URL("http://java.sun.com");
Vous pouvez donc utiliser DefaultFormatter pour mettre en forme les objets URL. Le formateur appelle toString() sur la valeur du champ pour initialiser le texte. Lorsque le champ perd le focus, le formateur construit un nouvel objet de la même classe que la valeur actuelle, en utilisant le constrcuteur avec un paramètre String. Si ce constructeur déclenche une exception, la modification n'est pas valide.
Par défaut, DefaultFormatter est en mode overwrite (mode de remplacement). Cette situation est différente pour les autres formateurs et finalement pas très utile. Appelez la méthode setOverwriteMode(false) pour désactiver le mode overwrite.
package format; import java.net.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.text.*; import javax.swing.text.DefaultFormatter; public class ValidationURL extends JFrame implements ActionListener { private DefaultFormatter format = new DefaultFormatter(); private JFormattedTextField saisie = new JFormattedTextField(format); private JTextField résultat = new JTextField("Saisissez votre adresse URL"); private URL url; public ValidationURL() throws MalformedURLException { super("Saisie de l'URL"); format.setOverwriteMode(false); saisie.setColumns(20); url = new URL("http:"); saisie.setValue(url); add(saisie, BorderLayout.NORTH); résultat.setEditable(false); add(résultat, BorderLayout.SOUTH); saisie.addActionListener(this); pack(); setResizable(false); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { if (saisie.isEditValid()) { résultat.setText("URL valide"); url = (URL) saisie.getValue(); } else résultat.setText("URL non valide"); } public static void main(String[] args) throws MalformedURLException { new ValidationURL(); } }
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 |
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.
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.
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.
Si l'une ou l'autre méthode détecte une erreur, elle lance une exception ParseException.
.
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.
@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.
.
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.
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.
Nous allons traiter deux cas de figure. D'une part, en filtrant un simple JTextField. D'autre part, en filtrant un JFormattedTextField.
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.
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é.
... 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é.
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.
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 :
Pour charger du texte dans le JEditorPane, nous peuvons utiliser une des méthodes suivantes :
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 :
JEditorPane éditeur = new JEditorPane(); // éditeur vierge
...
éditeur.setPage(new URL("http://www.unsite.fr")); // navigation vers une nouvelle page
JEditorPane éditeur = new JEditorPane(new URL("http://www.unsite.fr")); // proposer une page HTML au départ
InputStream fichier = new FileInputStream("fichier.html");
éditeur.read(fichier, null);
é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.
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(); } }
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.
(String)monDocHTML.getProperty(Document.TitleProperty);
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(); }
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(); }
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(); }
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(); }
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.
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 :
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.
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 ;
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 :
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 :
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 :
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(); } }
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.
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(); }
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 :
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 :
JTextPane texte = new JTextPane();
Style racine = texte.getStyle("default");
// 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);
Style nouveau = texte.addStyle("Nom du style", racine); // C'est vous qui choisissez l'intitulé du style.
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);
texte.setLogicalStyle(texte.getStyle("Italique")); // Attention, ce style va être opérationnel sur tout le document
texte.setParagraphAttributes(texte.getStyle("Justification"), true); // Attention, tous les paragraphes seront suivant ce même style
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) { ... }
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 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.
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(); } }
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. |
Cette interface étend AttributeSet pour ajouter des méthodes permettant de modifier l'ensemble des attributs et des parents.
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().
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.
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.
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(); } }
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(); } }
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.
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(); } }