Programmation Java Swing/Version imprimable

Ceci est la version imprimable de Programmation Java Swing.
  • Si vous imprimez cette page, choisissez « Aperçu avant impression » dans votre navigateur, ou cliquez sur le lien Version imprimable dans la boîte à outils, vous verrez cette page sans ce message, ni éléments de navigation sur la gauche ou en haut.
  • Cliquez sur Rafraîchir cette page pour obtenir la dernière version du wikilivre.
  • Pour plus d'informations sur les version imprimables, y compris la manière d'obtenir une version PDF, vous pouvez lire l'article Versions imprimables.


Programmation Java Swing

Une version à jour et éditable de ce livre est disponible sur Wikilivres,
une bibliothèque de livres pédagogiques, à l'URL :
https://fr.wikibooks.org/wiki/Programmation_Java_Swing

Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la Licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans Texte de dernière page de couverture. Une copie de cette licence est incluse dans l'annexe nommée « Licence de documentation libre GNU ».

Introduction

Principes

modifier

La bibliothèque de composants Swing est définie dans le package javax.swing et utilise AWT (java.awt) :

  • La gestion des évènements est celle des composants AWT basée sur les mêmes écouteurs d'évènements.
  • Les types Color, Font, Dimension, Insets... du package java.awt sont utilisés par Swing également.
  • Les gestionnaires de disposition des composants définis par AWT sont également utilisés par les conteneurs Swing.
  • Les composants Swing dérivent tous du composant de base défini par la classe javax.swing.JComponent. Cette classe dérive de la classe java.awt.Container. Cela signifie que tout composant Swing peut contenir d'autres composants. Cependant, le conteneur privilégié est le panel défini par la classe javax.swing.JPanel.

Le nom des classes Swing reprend le nom de la classe équivalente AWT préfixé par la lettre J : JComponent, JContainer, JPanel, JFrame...

 

Fenêtres simples

modifier

La fenêtre la plus simple est une classe vide héritant de la classe javax.swing.JFrame qui est vide par défaut.

Pour les opérations de mise à jour graphiques il vaut mieux utiliser le thread dédié (thread de répartition des évènements graphiques) pour éviter les problèmes de synchronisation, comme illustré par la méthode statique main ci-dessous pour créer et afficher la fenêtre :

package org.wikibooks.fr.swing;

import java.awt.*;

import javax.swing.*;

/**
 * Fenêtre simple.
 * @author fr.wikibooks.org
 */
public class FenetreSimple extends JFrame
{
	public static void main(String[] args)
	{
		// Utiliser le thread dédié à l'interface graphique
		// pour les opérations graphiques :
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				FenetreSimple frame = new FenetreSimple(); // Créer
				frame.setVisible(true); // Afficher
			}
		});
	}
}

Pour tester cet exemple :

  1. Copiez le code source dans un fichier nommé FenetreSimple.java dans une hiérarchie de répertoire org/wikibooks/fr/swing pour le paquetage déclaré ;
  2. Compilez-le avec la commande
    javac org.wikibooks.fr.swing.FenetreSimple
  3. Lancez le programme avec la commande
    java -cp . org.wikibooks.fr.swing.FenetreSimple
    ou comme il n'est pas nécessaire d'avoir une console :
    javaw -cp . org.wikibooks.fr.swing.FenetreSimple

La fenêtre ne comporte pas de titre, de taille réduite et située en haut à gauche, le bouton de fermeture permet toutefois de fermer la fenêtre. Cependant, l'application tourne encore. On s'en aperçoit en lançant l'application avec un IDE (tel Eclipse). Le thread de répartition des évènements graphiques continue de tourner.

Pour que l'application s'arrête (fin de tous les threads), il est nécessaire d'ajouter un écouteur d'évènement sur la fermeture de la fenêtre. L'exemple ci-dessous montre cela et comment définir un titre, la taille et la position de la fenêtre dans le constructeur de la classe. La méthode main de lancement de l'application reste la même :

package org.wikibooks.fr.swing;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

/**
 * Fenêtre simple.
 * @author fr.wikibooks.org
 */
public class SecondeFenetreSimple extends JFrame
{
	public SecondeFenetreSimple()
	{
		setTitle("Une deuxième fenêtre simple");
		setSize(new Dimension(600,400));
		setLocation(new Point(200, 100));
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter()
		{
			/* (non-Javadoc)
			 * @see java.awt.event.WindowAdapter#windowClosing(java.awt.event.WindowEvent)
			 */
			@Override
			public void windowClosing(WindowEvent e)
			{
				dispose();
				System.exit(0);
			}
		});
	}

	public static void main(String[] args)
	{
		// Utiliser le thread dédié à l'interface graphique
		// pour les opérations graphiques :
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				SecondeFenetreSimple frame = new SecondeFenetreSimple(); // Créer
				frame.setVisible(true); // Afficher
			}
		});
	}
}


Composants

De manière générale, un composant dans une interface graphique possède une zone rectangulaire permettant d'afficher une ou plusieurs informations et permet à l'utilisateur d'interagir avec lui en utilisant la souris ou le clavier.

 
Quelques composants Swing dans une fenêtre

Divers composants

modifier

Cette section liste brièvement quelques composants Swing décrits plus en détails dans les sections suivantes. Les composants Swing sont nombreux, seuls les plus courants seront décrits ici.

JLabel
Ce composant permet d'afficher du texte et une icône.
JButton
Ce composant (bouton) permet de déclencher une action par un clic de souris.
JToggleButton
Bouton basculant entre deux états.
JTextfield
Un champ de saisie de texte simple.
JPasswordField
Un champ de saisie de mot de passe dont les caractères sont cachés.
JTextArea
Une zone de saisie de texte.
JTextPane
Une zone d'affichage ou de saisie de texte formaté.
JCheckBox
Une case à cocher.
JRadioButton
Un item sélectionnable parmi un groupe.
JComboBox
Une liste déroulante de choix (un seul choix possible).
JList
Une liste d'items, un ou plusieurs items sélectionnable.
JSpinner
Champ de saisie avec boutons d'incrémentation et de décrémentation (nombre, items dans une liste prédéfinie, ...).
JSlider
Sélection d'une valeur numérique par glissement d'un curseur sur une ligne.
JProgressBar
Barre de progression.
JTree
Arborescence de nœuds pour les données structurées.
JTable
Table de données.
JScrollbar
Une barre de défilement.
JPanel
Un conteneur générique.
JScrollPane
Un conteneur permettant de faire défiler le contenu du composant (la vue) lorsque sa taille dépasse les limites du conteneur.
JSplitPane
Un conteneur avec deux composants séparés par un mince composant diviseur dont la position est réglable à la souris. Ce diviseur contient par défaut deux boutons permettant de cacher l'un des deux composants pour afficher l'autre sur toute la surface du conteneur.

Architecture logicielle

modifier

Les composants Swing utilisent l'architecture MVC (modèle vue contrôleur) :

  • La vue (view en anglais) contient la présentation de l'interface graphique.
La vue est gérée par le composant Swing, par exemple : JTable, JTree...
  • Le modèle (model en anglais) contient et gère les données à afficher.
Le modèle est associé au composant : JTableModel, JTreeModel...
  • Le contrôleur (controller en anglais) contient la logique concernant les actions effectuées par l'utilisateur.
Le rôle du contrôleur est réparti entre le code du composant définissant un comportement interne et les écouteurs d'évènements définis par l'application.

Taille et position

modifier

En général et par défaut, la taille et la position des composants dans un conteneur sont définies par un gestionnaire de disposition des composants. Elles ne peuvent donc être modifiées qu'en cas d'absence de gestionnaire de disposition (setLayout(null)). Cependant, il est recommandé d'utiliser l'un des gestionnaires de disposition des composants pré-existants.

Position

modifier

Comme en AWT, la position d'un composant est définie par deux coordonnées x et y, qui peuvent être représentées par un objet de classe java.awt.Point. Cette classe comporte deux champs publics x et y de type entier (int).

void setLocation(int x, int y) Cette méthode définit les coordonnées horizontales et verticales (respectivement) du coin supérieur gauche du composant. Les coordonnées sont relatives au conteneur ; c'est à dire que (0,0) correspond au bord supérieur gauche du conteneur, éventuellement espacé de la largeur du bord du conteneur.
void setLocation(Point p) Cette méthode définit les coordonnées en utilisant un objet de classe java.awt.Point.
Point getLocation() Cette méthode retourne la position du composant.

La taille d'un composant est définie par la largeur (width en anglais) et la hauteur (height en anglais), qui peuvent être représentées par un objet de classe java.awt.Dimension. Cette classe comporte deux champs publics width et height de type entier (int).

void setSize(int width, int height) Cette méthode définit la taille du composant.
void setLocation(Dimension size) Cette méthode définit la taille en utilisant un objet de classe java.awt.Dimension.
Dimension getSize() Cette méthode retourne la taille du composant.

Utiliser un objet au lieu de spécifier directement les valeurs permet de réutiliser les mêmes valeurs pour d'autres composants, ou pour définir la taille minimales, maximales et préférées d'un composants, ou de réutiliser les valeurs retournées par un composant.

Taille et position

modifier

La taille et la position peuvent être définies en une fois en utilisant les méthodes ci-dessous, soit en spécifiant les 4 valeurs, soit en utilisant un objet de classe java.awt.Rectangle. Cette classe comporte quatre champs publics de type entier (int) : x et y pour la position, et width et height pour la taille.

void setBounds(int x, int y, int width, int height) Cette méthode définit la taille et la position du composant.
void setBounds(Rectangle bounds) Cette méthode définit la taille et la position en utilisant un objet de classe java.awt.Rectangle.
Rectangle getBounds() Cette méthode retourne la taille et la position du composant.

Propriétés d'apparence

modifier

Certaines propriétés communes aux composants changent leur apparence :

void setBackground(Color c) / Color getBackground()
Couleur de fond du composant.
void setForeground(Color c) / Color getForeground()
Couleur de premier plan, utilisée pour le texte.
void setFont(Font f) / Font getFont()
Police de caractères par défaut pour le texte.
void setText(String s) / String getText()
Texte affiché (label, bouton, champ de saisie, ...).
void setBorder(Border b) / Border getBorder()
Ajoute un espace autour du composant pour y dessiner une bordure. La classe BorderFactory définit des méthodes statiques permettant de créer des bordures.
void setTooltipText(String s) / String getTooltipText()
Texte affiché temporairement au survol de la souris sur le composant.

Propriétés de comportement (état)

modifier
  • La méthode void setVisible(boolean b) permet de changer la visibilité du composant. La méthode boolean isVisible() retourne l'état de visibilité du composant.
  • La méthode void setEnabled(boolean b) permet de désactiver/griser (false) ou (ré)activer (true) un composant. La méthode boolean isEnabled() retourne l'état d'activation du composant.
  • La méthode void setEditable(boolean b) permet de désactiver (false) ou (ré)activer (true) la modification de la valeur dans un champ de saisie dérivant de la classe JTextComponent (classes JTextArea, JTextField, JFormattedTextField, JPasswordField, JEditorPane, JTextPane, ...). La méthode boolean isEditable() retourne l'état d'activation de modification de la valeur.

Suite à la réactivation d'un champ texte, soit par la méthode void setEnabled(true), soit par la méthode void setEditable(true), il peut arriver que le curseur n'apparaisse pas, même quand le focus est sur le champ. La solution, après réactivation, est de rendre visible le curseur explicitement :

textinput.getCaret().setVisible(true);

Composants

modifier

Pour tester rapidement les composants des sections qui suivent, voici un code source Java créant une fenêtre de test.

package org.wikibooks.fr.swing;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;

/**
 * Fenêtre pour tester les composants.
 * @author fr.wikibooks.org
 */
public class FenetreTestComposants extends JFrame
{
	public FenetreTestComposants()
	{
		// Configurer la fenêtre
		setTitle("Composants");
		setSize(new Dimension(600,400));  // Taille initiale de la fenêtre
		setLocation(new Point(200, 100)); // Position initiale de la fenêtre
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		// Création du panel pour le contenu de la fenêtre
		JPanel contentPane = new JPanel();
		contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(contentPane);

		// Copier/Écrire ici le code pour créer le composant comme celui donné dans les sections suivantes.
		// ...
		//    composant = new  ... ( ... );
		// ...

		contentPane.add(composant); // remplacer "composant" par le nom de la variable utilisée.
	}

	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				FenetreTestComposants frame = new FenetreTestComposants(); // Créer
				frame.setVisible(true); // Afficher
			}
		});
	}
}

Ce composant permet d'afficher du texte et une icône.

// Convention : l_ pour label
// Un label pour le nom de l'application :
JLabel l_nom_app = new JLabel("Apprentissage Java Swing sur fr.wikibooks.org");
// Les lignes suivantes sont optionnelles mais permettent de configurer le composant
l_nom_app .setFont(new Font("Tahoma", Font.BOLD, 36)); // Police "Tahoma", en gras, 36 pixels

Dans la fenêtre de test, une largeur de 600 pixels ne suffit pas à afficher tout le texte. Le composant coupe le texte avant 600 pixels et affiche des points de suspension.

Pour afficher un texte sur plusieurs lignes, l'utilisation de caractère de retour à la ligne \n ou \r n'ont aucun effet. Il faut utiliser un format HTML :

// Convention : l_ pour label
// Un label pour le nom de l'application :
JLabel l_nom_app = new JLabel("<html>Apprentissage Java Swing<br>sur fr.wikibooks.org</html>");
// Les lignes suivantes sont optionnelles mais permettent de configurer le composant
l_nom_app .setFont(new Font("Tahoma", Font.BOLD, 36)); // Police "Tahoma", en gras, 36 pixels

Le constructeur a trois paramètres optionnels : JLabel(String text, Icon icon, int horizontalAlignment).

text
Texte affiché, aucun si absent.
icon
Image affichée, aucune si absent. Si le texte est aussi spécifié, l'image est à gauche du texte par défaut.
La classe ImageIcon est une sous-classe Icon et définit l'icône à partir d'une image de classe Image.
horizontalAlignment
Alignement du contenu :
  • SwingConstants.LEFT // À gauche
  • SwingConstants.CENTER // Centré
  • SwingConstants.RIGHT // À droite
  • SwingConstants.LEADING // En début de ligne (gauche / droite)
  • SwingConstants.TRAILING // En fin de ligne (droite / gauche)

Quand un texte et une image sont affichés, l'image est à gauche du texte par défaut. La méthode setHorizontalTextPosition(int textPosition) permet de choisir la position du texte relativement à l'image :

  • SwingConstants.LEFT // À gauche
  • SwingConstants.CENTER // Centré
  • SwingConstants.RIGHT // À droite
  • SwingConstants.LEADING // Du côté du début de ligne (gauche / droite)
  • SwingConstants.TRAILING // Du côté de la fin de ligne (droite / gauche)

Label activant un composant

modifier

Un label peut être utilisé pour activer le composant associé. Par exemple, mettre le focus sur un champ de saisie quand l'utilisateur tape la touche mnémonique associée au label.

setDisplayedMnemonic(int key)
Spécifier un code de touche mnémonique à afficher.
setLabelFor(Component c)
Spécifier le composant associé au label.

JButton

modifier

Ce composant permet de déclencher une action par un clic de souris.

// Convention : b_ pour button
JButton b_configurer = new JButton("Configurer...");
b_configurer.addActionListener(new ActionListener()
{
	public void actionPerformed(ActionEvent e)
	{ configurer(); }
});

// N.B. : Définir la méthode configurer() appelée par le bouton dans la classe

JTextfield

modifier

Un champ de saisie de texte simple. La classe JTextfield hérite de JTextComponent définissant les méthodes pour les champs de saisie.

// Convention : tf_ pour text field
JTextField tf_titre_livre = new JTextField();

// Ajouter une bulle d'information pour l'utilisateur s'affichant au survol de la souris :
tf_titre_livre.setToolTipText("Titre d'un livre");

String livre = "Programmation Java Swing";
tf_titre_livre.setText(livre); // Initialiser la valeur dans le champ de saisie

// Plus tard, lors d'une action :
livre = tf_titre_livre.getText(); // Récupérer la valeur du champ saisie par l'utilisateur

JPasswordField

modifier

Un champ permettant la saisie d'un mot de passe, cachant les caractères tapés. La classe JPasswordField hérite de JTextComponent définissant les méthodes pour les champs de saisie. Cette classe possède donc les mêmes méthodes que JTextfield.

// Convention : pf_ pour password field
JPasswordField pf_connexion = new JPasswordField(); // Mot de passe de connexion

// Ajouter une bulle d'information pour l'utilisateur s'affichant au survol de la souris :
pf_connexion.setToolTipText("Entrez le mot de passe de connexion");

pf_connexion.setText(""); // Initialiser la valeur dans le champ de saisie

// Plus tard, lors d'une action :

// String mot = pf_connexion.getText(); // Récupérer la valeur du champ saisie par l'utilisateur
// Obsolète, ne pas utiliser dans les applications sécurisées, voir ci-dessous.

char[] mot = pf_connexion.getPassword(); // Récupérer la valeur du champ saisie par l'utilisateur
// Comme mentionné dans la documentation de la méthode getPassword(),
// pour plus de sécurité, il est recommandé d'effacer le contenu
// du tableau retourné après usage en le remplissant de zéro.
Arrays.fill(mot, '\0');
// Ceci n'est pas possible avec le type String immuable, expliquant pourquoi
// il ne faut pas utiliser la méthode getText().

JTextArea

modifier

Une zone de saisie de texte.

JTextPane

modifier

Une zone d'affichage ou de saisie de texte formaté.

JCheckBox

modifier

Une case à cocher.

// Convention : cb_ pour check box
cb_resume = new JCheckBox("Avec résumé");

// Ajouter une bulle d'information pour l'utilisateur s'affichant au survol de la souris :
cb_resume.setToolTipText("Générer un résumé du livre");

cb_resume.setSelected(true);  // Initialiser la case : cochée [X]
cb_resume.setSelected(false); // ou non cochée [ ]

// Plus tard, lors d'une action :
boolean avec_resume = cb_resume.isSelected(); // Récupérer l'état choisi par l'utilisateur

Ce composant gère deux états, mais pas d'état intermédiaire (coché partiellement ou indéterminé). Mais il est possible de créer un composant ayant ce troisième état : voir le chapitre avancé « Créer une case à cocher à trois états ».

JRadioButton

modifier

Un bouton radio, permettant de sélectionner un item parmi plusieurs dans un groupe. La classe JRadioButton possède plusieurs constructeurs qui permettent d'initialiser le texte affiché, l'image d'icône, et l'état initial (booléen, sélectionné ou non).

new JRadioButton(String text, Icon icon, boolean selected)
Tous les paramètres sont optionnels.
new JRadioButton(Action action)
Paramétrage avec une action.

La classe ButtonGroup n'est pas un composant mais représente un groupe de boutons abstraits (JRadioButton, JToggleButton, ...) dont seul l'un d'entre eux peut être sélectionné à la fois. La sélection de l'un d'eux entraîne la dé-sélection des autres.

Exemple :

// Convention : bg_ pour button group
ButtonGroup bg_theme = new ButtonGroup();

// Convention : rb_ pour radio button
JRadioButton rb_theme_clair = new JRadioButton("Thème clair");
bg_theme.add(rb_theme_clair); // Ajout au groupe

JRadioButton rb_theme_sombre = new JRadioButton("Thème sombre");
bg_theme.add(rb_theme_sombre); // Ajout au groupe

JRadioButton rb_theme_sepia = new JRadioButton("Thème sépia");
bg_theme.add(rb_theme_sepia); // Ajout au groupe

Gestion de sélection :

bg_theme.clearSelection(); // Tous dé-sélectionnés

// Sélectionner un bouton radio :
rb_theme_clair.setSelected(true);

// Connaître l'état de sélection :
boolean avec_theme_clair = rb_theme_clair.isSelected();

JComboBox

modifier

Une liste déroulante de choix (un seul choix possible).

Une liste d'items, un ou plusieurs items sélectionnable.

Le constructeur accepte un tableau d'objets (leur méthode toString() est appelé pour le texte à afficher) ou un modèle de liste JListModel. L'utilisation d'un modèle de liste est recommandée quand la liste des items est modifiable par l'utilisateur.

final Object[] JOURS = { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };
//  Indice :                0        1         2          3         4          5          6

// Convention : ls_ pour list
JList ls_jours = new JList(JOURS);

Ce composant a plusieurs modes de sélection :

  • un seul item sélectionnable :
// Mode de sélection : un seul
ls_jours.setselectionMode(ListSelectionModel.SINGLE_SELECTION);
ls_jours.setSelectedIndex(2); // Mercredi

// Plus tard, durant une action :
int index_jour = ls_jours.getSelectedIndex();
  • un seul intervalle continu sélectionnable :
// Mode de sélection : un seul intervalle
ls_jours.setselectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
ls_jours.setSelectionInterval(2, 4); // Du mercredi au vendredi inclus

// Plus tard, durant une action :
int[] indexs_jour = ls_jours.getSelectedIndices();
  • plusieurs intervalles sélectionnables :
// Mode de sélection : plusieurs intervalles
ls_jours.setselectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

// Soit tous les indices :
ls_jours.setSelectedIndices(new int[]{0,1,2, 4,5}); // Lundi, mardi, mercredi, vendredi, samedi

// Soit les intervalles :
ls_jours.setSelectionInterval(0, 2); // Du lundi au mercredi inclus
ls_jours.addSelectionInterval(4, 5); // et ajouter du vendredi au samedi inclus

// Plus tard, durant une action :
int[] indexs_jour = ls_jours.getSelectedIndices();

La taille de la liste est déterminée par le nombre d'items. Ce composant ne gère pas le défilement. Il faut l'englober dans un composant JScrollPane pour gérer le défilement d'une longue liste dans un espace plus réduit.

JSpinner

modifier

Champ de saisie avec boutons d'incrémentation et de décrémentation (nombre, items dans une liste prédéfinie, ...).

JSlider

modifier

Sélection d'une valeur numérique par glissement d'un curseur sur une ligne.

JProgressBar

modifier

Barre de progression.

Arborescence de nœuds pour les données structurées.

Table de données.

JScrollbar

modifier

Une barre de défilement.

Ce composant est rarement utilisé directement car il est de bas niveau et est utilisé notamment par JScrollPane qui gère la vue partielle, le défilement et les entêtes.

Un conteneur générique, comme le contentPane de la fenêtre de test.

Un panel, comme sa classe de base java.awt.Container, peut contenir tout type de composant, y compris d'autres panels. L'imbrication de panels permet de subdiviser un panel en plusieurs panels indépendant et réutilisables ayant leur propre configuration (gestionnaire de disposition, couleur de fond, ...).

// Convention : p_ pour panel
JPanel p_outils = new JPanel(); // un panel pour les outils de l'application
// Configuration :
p_outils.setBorder(new EmptyBorder(5, 5, 5, 5)); // bordure vide de 5 pixels dans les 4 directions
p_outils.setLayout(new FlowLayout()); // gestionnaire de disposition

JScrollPane

modifier

Un conteneur permettant de faire défiler le contenu du composant (la vue) lorsque sa taille dépasse les limites du conteneur. Ce type de panel est notamment utilisé avec les listes ayant plus de 3 ou 5 items, les tables, les arborescences, les panels ayant un contenu occupant une grande surface.

Ce composant comporte en fait neufs zones illustrées ci-dessous :

Coin

supérieur

gauche

 

Entête de colonne


Coin

supérieur

droit

Entête de ligne
 

Partie principale

du composant

Barre de défilement vertical
Coin

inférieur

gauche

 

Barre de défilement horizontal

Coin

inférieur

droit

Partie principale du composant
Il s'agit en général du composant lui-même, dont la vue suit le défilement horizontal et vertical des barres situées en bas et à droite respectivement.
Le composant affiché est celui passé au constructeur :
new JScrollPane(table_pages) // Pour la table des pages
Il peut être spécifié ou modifié après construction en utilisant la méthode setViewportView :
// Convention : sp_ pour scroll pane
JScrollPane sp_pages = new JScrollPane();
sp_pages.setViewportView(table_pages); // configurer la vue (view) du JViewport
Les trois zones de vues avec défilement (principal, entête de colonne, entête de ligne) sont en fait encapsulées dans un JViewport qui gère l'affichage partiel. Il est possible de spécifier un autre type de JViewport en utilisant la méthode setViewport.
Entête de colonne
Un entête toujours affiché en haut, mais pouvant être défilé horizontalement pour suivre le contenu principal.
Il peut s'agir d'un composant quelconque. Cependant, pour les tables, il s'agit souvent de l'entête des colonnes de la table que l'on obtient par appel de la méthode getTableHeader :
sp_pages.setColumnHeaderView(table_pages.getTableHeader()); // configurer l'entête de colonne
Les trois zones de vues avec défilement (principal, entête de colonne, entête de ligne) sont en fait encapsulées dans un JViewport qui gère l'affichage partiel. Il est possible de spécifier un autre type de JViewport en utilisant la méthode setColumnHeader.
Entête de ligne
Un entête toujours affiché à gauche, mais pouvant être défilé verticalement pour suivre le contenu principal.
Il s'agit d'un composant quelconque :
sp_pages.setRowHeaderView(p_row); // configurer l'entête de ligne
Les trois zones de vues avec défilement (principal, entête de colonne, entête de ligne) sont en fait encapsulées dans un JViewport qui gère l'affichage partiel. Il est possible de spécifier un autre type de JViewport en utilisant la méthode setRowHeader.
Barres de défilement
Les barres de défilement sont affichées quand nécessaire par défaut. Ce comportement peut être modifié soit en spécifiant le mode pour les barres verticales et horizontales au constructeur, ou en appelant les méthodes setVerticalScrollBarPolicy( ... ) et setHorizontalScrollBarPolicy( ... ) :
setVerticalScrollBarPolicy( ... ) setHorizontalScrollBarPolicy( ... ) Quand afficher la barre de défilement vertical / horizontal :
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED Quand nécessaire (contenu plus large que la zone de vue).
ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER Jamais (vue tronquée et non défilable).
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS Toujours.
Coins
Il peut y avoir jusqu'à quatre coins affichés, qui sont par défaut vides. Il est possible d'y placer des composants de petite taille, en général décoratifs seulement, afin de combler le vide.
setCorner(String key, Component component)
key
Nom du coin où placer le composant. Ce nom doit être l'un de ceux listés ci-dessous :
  • Coin absolu :
    • ScrollPaneConstants.UPPER_LEFT_CORNER // supérieur gauche
    • ScrollPaneConstants.UPPER_RIGHT_CORNER // supérieur droit
    • ScrollPaneConstants.LOWER_LEFT_CORNER // inférieur gauche
    • ScrollPaneConstants.LOWER_RIGHT_CORNER // inférieur
  • Coin relatif au sens de lecture :
    • ScrollPaneConstants.UPPER_LEADING_CORNER // supérieur début de ligne (gauche/droit)
    • ScrollPaneConstants.UPPER_TRAILING_CORNER // supérieur fin de ligne (droit/gauche)
    • ScrollPaneConstants.LOWER_LEADING_CORNER // inférieur début de ligne (gauche/droit)
    • ScrollPaneConstants.LOWER_TRAILING_CORNER // inférieur fin de ligne (droit/gauche)
component
Composant à placer.


Arbre

Swing possède une classe de composant graphique permettant l'affichage d'une arborescence de données, de manière similaire à un explorateur de répertoires, nommée JTree.

Modèle

modifier

Le modèle de l'arbre est géré par une classe implémentant l'interface javax.swing.tree.TreeModel. Cette interface permet d'obtenir l'arborescence des objets à afficher, la notification de modification des objets, et d'enregistrer un écouteur d'évènement de changement d'arborescence. Elle est implémentée par la classe javax.swing.tree.DefaultTreeModel gérant des nœuds implémentant l'interface javax.swing.tree.TreeNode.

Cette interface est implémentée par la classe javax.swing.tree.DefaultMutableTreeNode.




Fenêtres

Les fenêtres Swing dérivent de celles disponibles avec AWT :

  • JFrame (sous-classe de Frame) représente une fenêtre d'application.
  • JDialog (sous-classe de Dialog) représente une fenêtre de dialogue.
  • JWindow (sous-classe de Window) représente une fenêtre générique.
  • JInternalFrame (pas d'équivalent AWT) représente une fenêtre interne à l'intérieur d'une zone multi-documents gérée par la classe JDesktopPane.
  • JApplet (sous-classe de Applet) représente une fenêtre intégrée dans une page HTML d'un navigateur web. Ce type de fenêtre ne se rencontre plus beaucoup car pour des raisons de sécurité, Java est désactivé par défaut sur tous les navigateurs récents. De plus les applets sont déclarées obsolètes depuis Java 9 en 2017, et supprimées de Java depuis la version 11.

Points communs

modifier

Les différents types de fenêtres Swing cités précédemment ont une structure commune et des méthodes communes.

L'exemple ci-dessous crée une fenêtre d'application avec un titre, contenant un label affichant du texte.

package org.wikibooks.fr.swing;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

/**
 * Créer une fenêtre. 
 * @author fr.wikibooks.org
 */
public class ExempleFenetre extends JFrame
{
	public ExempleFenetre()
	{
		// Création de la fenêtre
		setTitle("Démonstration de création de fenêtre Swing");
		Dimension d = new Dimension(500, 200);
		setMinimumSize(d);
		setSize(d);

		Container c = getContentPane();
		c.setLayout(new FlowLayout());

		JLabel l_texte = new JLabel("La fenêtre est ouverte");
		c.add(l_texte);

	}

	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				ExempleFenetre frame = new ExempleFenetre();
				frame.setVisible(true);
			}
		});
	}
}

Titre et icône

modifier

Une fenêtre possède un titre (méthode setTitle(String)) et une icône (méthode setIconImage(Image)). Par défaut, l'icône est la même que celle de la fenêtre propriétaire.

Composition

modifier

Chaque fenêtre comporte un panel racine (classe JRootPane) qui gère la composition des éléments suivants :

  • Le panneau frontal (Glass pane en anglais) est affiché au premier plan. Il est généralement utilisé pour afficher une animation de chargement de la fenêtre, une progression d'une longue opération en cours.
  • Le panneau de contenu (Content pane) contient les composants ajoutés à la fenêtre (bouton, label, ...).
  • La barre de menu (JMenuBar) définit les listes d'actions utilisables. Elle est affichée en haut de la fenêtre.

La barre de menu et le panneau de contenu sont gérés par un panneau de classe JLayeredPane permettant de gérer la superposition de composants.

Taille et position

modifier

La taille et la position d'une fenêtre sont gérées avec les mêmes méthodes que celles des composants. Il n'y a pas de gestionnaire de disposition pour les fenêtres, car aucun conteneur ne les contient (hormis JInternalFrame et JApplet).

La taille et la position de la fenêtre définies par l'application peuvent ensuite être modifiées par l'utilisateur en déplaçant et en redimensionnant la fenêtre. Les méthodes setMinimumSize(Dimension) et setMaximumSize(Dimension) définissent les limites du redimensionnement. La méthode setPreferredSize(Dimension) héritée de la classe Component n'a pas de sens pour les fenêtres.

Les fenêtres de type javax.swing.JFrame et javax.swing.JDialog peuvent avoir une barre de menus.

Voir le chapitre suivant sur les menus pour plus de détails.

Fenêtre d'application

modifier

La fenêtre d'application est celle qui définie la vue principale d'une application. Une application peut en avoir plusieurs (Par exemple, une fenêtre par fichier ouvert). Les applications les plus simples n'ont qu'une seule fenêtre principale.

La classe javax.swing.JFrame définit une fenêtre d'application.

Action de fermeture

modifier

La méthode setDefaultCloseOperation(int operation) définit l'action effectuée quand l'utilisateur clique le bouton de fermeture de la fenêtre. L'argument peut avoir l'une des valeurs suivantes définies dans l'interface WindowConstants implémentée par la classe JFrame :

DO_NOTHING_ON_CLOSE
Ne rien faire, et appeler les gestionnaires d'évènements des fenêtres (WindowListener) enregistrés.
HIDE_ON_CLOSE
Cacher la fenêtre puis appeler les gestionnaires d'évènements des fenêtres (WindowListener) enregistrés.
DISPOSE_ON_CLOSE
Cacher et libérer la fenêtre puis appeler les gestionnaires d'évènements des fenêtres (WindowListener) enregistrés.
EXIT_ON_CLOSE
Terminer l'application en appelant System.exit(0). Les gestionnaires d'évènements des fenêtres ne sont pas appelés.

Par défaut, la valeur est HIDE_ON_CLOSE. Tout changement provoque un évènement de changement de propriété dont le nom est "defaultCloseOperation".

Quand la dernière fenêtre est libérée (dispose), l'application peut se terminer.

Quand l'application doit faire confirmer la fermeture par l'utilisateur (par exemple en cas de données non sauvegardées), il est nécessaire de définir l'action de fermeture à rien (DO_NOTHING_ON_CLOSE) et d'ajouter un écouteur d'évènements des fenêtres (interface WindowListener implémentée par la classe vide WindowAdapter).

Dans le constructeur de la classe de fenêtre principale :

addWindowListener(new WindowAdapter()
{
	@Override
	public void windowClosing(WindowEvent e)
	{
		fermerFenetre();
	}
});

Dans la classe de fenêtre principale :

// Indicateur de données modifiées depuis la dernière sauvegarde
private boolean donnees_modifiees = false;
// Assigné à false par les actions suivantes :
//    - création d'un nouveau fichier,
//    - chargement à partir d'un fichier,
//    - enregistrement des données.
// Assigné à true par toute action modifiant les données.

// Pour confirmer une action faisant perdre les données
private boolean confirmerAction(String action)
{
	if (donnees_modifiees)
	{
		int reponse = JOptionPane.showConfirmDialog(this,
			"Attention ! Les données n'ont pas été sauvegardées.\n"+
			action+" maintenant provoquera une perte de données.\n"+
			"Continuer et perdre les données ?",
			"Confirmer la perte de données",
			JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
		return reponse == JOptionPane.YES_OPTION;
	}
	else return true; // On peut effectuer l'action
}

private void fermerFenetre()
{
	if (confirmerAction("Quitter l'application"))
	{
		// Fermer la fenêtre et quitter
		dispose();
		System.exit(0);
	}
}

La méthode confirmerAction ci-dessus est réutilisable pour les actions provoquant une perte des données non sauvegardées :

  • La fermeture de la fenêtre principale,
  • L'action quitter (menu / bouton),
  • Charger un fichier (si la même fenêtre est réutilisée),
  • Créer un nouveau fichier (si la même fenêtre est réutilisée),
  • ...

États de la fenêtre

modifier

La méthode setExtendedState(int state) permet de modifier l'état de la fenêtre, et la méthode int getExtendedState() permet de récupérer l'état actuel :

NORMAL
Affichage normal,
ICONIFIED
Fenêtre icônifiée sur le bureau ou réduite dans la barre des tâches,
MAXIMIZED_HORIZ
Fenêtre affichée sur toute la largeur de l'écran,
MAXIMIZED_VERT
Fenêtre affichée sur toute la hauteur de l'écran,
MAXIMIZED_BOTH
Fenêtre affichée sur tout l'écran (combine les deux états précédents).

Icône de fenêtre

modifier

L'icône de la fenêtre d'application est visible, selon le système d'exploitation, dans le coin du bord de la fenêtre (sous Windows, en haut à gauche en 16x16 pixels), dans la barre des tâches (taille variable : 16x16, 48x48, 64x64, ...), dans le sélecteur de fenêtres (sous Windows avec AltTab ↹ ou Tab ↹). L'icône de la fenêtre d'application peut aussi être utilisée par les boîtes de dialogue ayant la fenêtre comme propriétaire (owner) passé au constructeur.

Deux méthodes existent :

  • La méthode setIconImage(java.awt.Image image) définit une image comme icône. Cette image est redimensionnée selon la taille nécessaire. Il est donc recommandé d'utiliser une image de grande résolution (64x64 pixels minimum) afin d'éviter un agrandissement produisant des blocs de pixels. Celle-ci doit donc aussi pouvoir être réduite sans perte de visibilité.
  • La méthode setIconImages(java.util.List<java.awt.Image> images) permet de définir une liste d'images. Selon la taille d'icône du contexte (exemple : 16x16 pour l'icône du bord de fenêtre, 64x64 pour la barre des tâches, ...), une image suffisamment grande est sélectionnée et redimensionnée si nécessaire. Cette méthode est recommandée si l'icône change en fonction de la taille, pour par exemple y mettre moins d'éléments pour les plus petites tailles d'icône.

La méthode setIconImage(java.awt.Image image) est en fait un appel à la méthode setIconImages(java.util.List<java.awt.Image> images) en lui passant une liste contenant une seule image.

En général, l'icône est une image stockée dans le même répertoire que les classes de l'application, dans un format supporté par ImageIO (PNG, GIF). Exemple pour une image stockée dans le même répertoire que la classe de la fenêtre :

/**
 * Créer la fenêtre principale.
 * @throws IOException Erreur de lecture de ressource interne.
 */
public MainWindow() throws IOException
{
	Image image_icon = ImageIO.read(MainWindow.class.getResource("icone_application.png"));
	setIconImage(image_icon);
}

Pour une liste d'icônes :

public MainWindow() throws IOException
{
	List<Image> liste_icones = new ArrayList<>();
	liste_icones.add(ImageIO.read(MainWindow.class.getResource("icone_application_16.png"))); // 16x16
	liste_icones.add(ImageIO.read(MainWindow.class.getResource("icone_application_48.png"))); // 48x48
	liste_icones.add(ImageIO.read(MainWindow.class.getResource("icone_application_64.png"))); // 64x64
	setIconImages(liste_icones);
}

Fenêtre de dialogue

modifier

Une fenêtre de dialogue est une fenêtre temporaire ouverte sur une action spécifique permettant de dialoguer avec l'utilisateur pour lui fournir et lui demander une ou plusieurs informations : entrer les champs d'une nouvelle entité à ajouter dans une table, sélectionner un fichier, sélectionner une couleur, afficher un message d'erreur, demander une confirmation de suppression...

Fenêtres de dialogue prédéfinies

modifier

Swing possède des classes ou méthodes permettant d'utiliser des fenêtres de dialogue prédéfinies.

JOptionPane
La classe JOptionPane définit une fenêtre de dialogue simple permettant d'afficher un message avec une icône prédéfinie associée (information, question, avertissement, erreur...), avec éventuellement un champ de saisie, et de 1 à 3 boutons pour divers choix possible de réponse.
Cette classe possède différentes méthodes statiques pour créer et afficher ces fenêtres.
JColorChooser
La classe JColorChooser définit une fenêtre de dialogue pour sélectionner une couleur.
JFileChooser
La classe JFileChooser définit une fenêtre de dialogue pour sélectionner un fichier ou un répertoire.

Fenêtre de dialogue de type message : JOptionPane

modifier

La classe JOptionPane définit une fenêtre de dialogue simple permettant d'afficher un message avec une icône prédéfinie associée, pouvant avoir un champ de saisie selon le type, et proposant plusieurs choix de bouton.

Simple notification de message

modifier

Ce type de fenêtre affiche simplement le message et possède un bouton OK pour fermer la fenêtre, et utilise la méthode showMessageDialog.

Exemple :

JOptionPane.showMessageDialog(frame,
    "Une erreur s'est produite pour illustrer cette section.\nUne seconde ligne d'information.",
    "Erreur de démonstration", JOptionPane.ERROR_MESSAGE);
  • La méthode showMessageDialog ne retourne aucune valeur.
  • frame est la fenêtre (Frame ou JFrame) par dessus laquelle le message est affiché. Si la référence est null, une fenêtre est créée.
  • Le message affiché dans la fenêtre est spécifié avant le titre.
  • Le dernier argument spécifie le type de message, définissant l'icône affichée en face du message :
JOptionPane.showMessageDialog(frame,
    "Un avertissement signale un fait important porté à l'attention de l'utilisateur.",
    "Avertissement de démonstration", JOptionPane.WARNING_MESSAGE);

JOptionPane.showMessageDialog(frame,
    "Une information pour l'utilisateur.",
    "Information de démonstration", JOptionPane.INFORMATION_MESSAGE);

JOptionPane.showMessageDialog(frame,
    "Un message pour l'utilisateur, sans icône.",
    "Message de démonstration", JOptionPane.PLAIN_MESSAGE);

// Le type question est plus pertinent pour les autres types de fenêtres où l'utilisateur peut choisir une réponse.
JOptionPane.showMessageDialog(frame,
    "Une question pour l'utilisateur, mais il ne peut répondre que OK.",
    "Des questions ?", JOptionPane.QUESTION_MESSAGE);

Il existe deux variantes de la méthode showMessageDialog :

  • Sans les deux derniers arguments de titre ("Message" par défaut) et type (JOptionPane.INFORMATION_MESSAGE par défaut).
  • Avec un cinquième argument permettant d'afficher une icône personnalisée de type xjava.swing.Icon.

Message en HTML

modifier

Il est possible d'utiliser le format HTML pour formater le message affiché. Pour cela, il faut encadrer le message dans une balise <html> et ne pas utiliser de caractères de contrôle dans le message ; par exemple "\n" qui doit être remplacé par <br/>.

Exemple :

JOptionPane.showMessageDialog(frame,
    "<html>Une erreur s'est produite pour illustrer cette section.<br/>" +
    "<b>Ceci est une information importante.</b></html>",
    "Erreur de démonstration", JOptionPane.ERROR_MESSAGE);

Pour plus de détails sur le formatage en HTML et les limitations, voir le chapitre sur le contenu en HTML.

Fenêtre de confirmation

modifier

Ce type de fenêtre affiche le message et propose plusieurs boutons de confirmation pour fermer la fenêtre, et utilise la méthode showConfirmDialog.

Exemple :

int reponse = JOptionPane.showConfirmDialog(frame,
    "Confirmez-vous vouloir fermer la fenêtre sans sauvegarder ?",
    "Modifications non sauvegardées",
    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);

Les variantes de la méthode sont :

int showConfirmDialog(Component parent, String message)
Titre "Select an option", boutons Oui, Non et Annuler ;
int showConfirmDialog(Component parent, String message, String title, int optionType)
Titre et boutons spécifiés ;
int showConfirmDialog(Component parent, String message, String title, int optionType, int messageType)
Type de message spécifié également ;
int showConfirmDialog(Component parent, String message, String title, int optionType, int messageType, Icon icon)
Avec icône personnalisée.
  • optionType définit les boutons disponibles :
    • JOptionPane.YES_NO_OPTION : Boutons Oui et Non ;
    • JOptionPane.YES_NO_CANCEL_OPTION : Boutons Oui, Non et Annuler ;
    • JOptionPane.OK_CANCEL_OPTION : : Boutons OK et Annuler.
  • La méthode retourne le choix de l'utilisateur :
    • JOptionPane.OK_OPTION : Bouton OK cliqué ;
    • JOptionPane.YES_OPTION : Bouton Oui cliqué ;
    • JOptionPane.NO_OPTION : Bouton Non cliqué ;
    • JOptionPane.CANCEL_OPTION : Bouton Annuler cliqué ;
    • JOptionPane.CLOSED_OPTION : Fenêtre fermée sans choix (à considérer comme Annuler ou Non).

Fenêtre de saisie

modifier

Ce type de fenêtre utilise la méthode showInputDialog et affiche le message et un champ de saisie ou une liste de choix, et les boutons OK et Cancel pour fermer la fenêtre.

Exemple :

String choix = JOptionPane.showInputDialog(frame,
    "Choisissez un nom pour le projet",
    "Choisir un nom",
    JOptionPane.QUESTION_MESSAGE,
    null,           // Pas d'icône personnalisée
    new Object[]{   // Choix possible
        "Wikilivres",
        "Wikipédia",
        "Wiktionnaire",
        null        // null pour autoriser l'utilisateur à entrer une autre valeur
    },
    // Par défaut :
    "Wikilivres");

La méthode retourne la valeur sélectionnée ou entrée par l'utilisateur, ou null si l'utilisateur a choisi d'annuler.

Panel de dialogue interne

modifier

Les méthodes vu précédemment ont toutes une variante pour afficher un panel de dialogue interne, dont le nom est celui de la méthode où show est remplacé par showInternal :

  • showInternalConfirmDialog,
  • showInternalMessageDialog,
  • showInternalInputDialog,
  • showInternalOptionDialog.

Fenêtre de sélection de couleur : JColorChooser

modifier

La fenêtre de sélection de couleur permet de choisir une couleur parmi une palette prédéfinie ou en spécifiant les composantes RVB ou TSL.

Les arguments de la méthode showDialog de la classe JColorChooser sont :

  • la fenêtre principale sur laquelle la fenêtre de dialogue s'affiche,
  • le titre de la fenêtre de dialogue,
  • la couleur initiale.

La méthode retourne la couleur sélectionnée de classe java.awt.Color ou null si l'utilisateur a annulé.

Exemple : Sélection de la couleur de fond :

Color c_fond = Color.BLACK;

Color c = JColorChooser.showDialog(frame, "Sélectionnez la couleur de fond", c_fond);
if (c != null)
{
    c_fond = c;
    setBackground(c); // ... ou autre mise à jour de couleur
}

Fenêtre de sélection de fichier ou répertoire : JFileChooser

modifier

La classe JFileChooser définit une fenêtre permettant de sélectionner un ou plusieurs fichiers, ou un répertoire.

Exemple : Sélection pour ouvrir un fichier.

protected File selectionFichierOuvrir()
{
    // Création
    JFileChooser jfc = new JFileChooser();
    // Sélection pour ouvrir un fichier :
    int res = jfc.showOpenDialog(frame); // Retourne le bouton cliqué
    return (res == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFile() : null;
}

Exemple : Sélection pour enregistrer un fichier.

protected File selectionFichierEnregistrer()
{
    // Création
    JFileChooser jfc = new JFileChooser();
    // Sélection pour enregistrer un fichier :
    int res = jfc.showSaveDialog(frame); // Retourne le bouton cliqué
    return (res == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFile() : null;
}

Pour conserver le répertoire courant à chaque réouverture, il vaut mieux que l'instance de la classe JFileChooser soit déclarée dans la classe et construit par le constructeur.

// Création
JFileChooser jfc = new JFileChooser();

protected File selectionFichierOuvrir()
{
    // Sélection pour ouvrir un fichier :
    int res = jfc.showOpenDialog(frame); // Retourne le bouton cliqué
    return (res == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFile() : null;
}

protected File selectionFichierEnregistrer()
{
    // Sélection pour enregistrer un fichier :
    int res = jfc.showSaveDialog(frame); // Retourne le bouton cliqué
    return (res == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFile() : null;
}

Fichier présélectionné

modifier

Avant l'affichage, on peut changer le répertoire courant et/ou le fichier sélectionné :

// Initialise le fichier sélectionné :
jfc.setSelectedFile(new File("/etc/config/wikilivres.fr.cfg"));
int res = jfc.showOpenDialog(frame); // Retourne le bouton cliqué
// Initialiser le répertoire courant :
jfc.setCurrentDirectory(new File("/etc/config"));
int res = jfc.showOpenDialog(frame); // Retourne le bouton cliqué

Sélectionner plusieurs fichiers

modifier

Il est possible d'autoriser la sélection de plusieurs fichiers :

protected File[] selectionPlusieursFichiersOuvrir()
{
    jfc.setMultiSelectionEnabled(true);
    int res = jfc.showOpenDialog(frame); // Retourne le bouton cliqué
    return (res == JFileChooser.APPROVE_OPTION) ? jfc.getSelectedFiles() : null; // getSelectedFiles au pluriel
}

Sélectionner un fichier ou un répertoire

modifier

Le mode de sélection permet de choisir si l'utilisateur peut sélectionner un fichier, un répertoire ou les deux :

jfc.setFileSelectionMode(JFileChooser.FILES_ONLY);
jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);

Filtres de fichier

modifier

La fenêtre de sélection possède deux types de filtre de classe javax.swing.filechooser.FileFilter :

  • Un filtre général pour ne pas afficher certains fichiers à l'utilisateur,
  • Une liste de filtre que l'utilisateur peut choisir pour afficher différents types de fichiers.

Le filtre général de la vue est configuré en appelant la méthode setFileFilter(FileFilter filter). Exemple :

jfc.setFilter(new javax.swing.filechooser.FileFilter()
    {
        @Override
        public boolean accept(File f)
        {
            return f.getName().toLowerCase().endsWith(".wiki") || f.isDirectory();
        }
        @Override
        public String getDescription()
        {
            return "Wiki source file"; // Description non utilisée pour le filtre général
        }
    });

Les filtres sélectionnables sont ajoutés un par un en appelant la méthode addChoosableFileFilter(FileFilter filter). Exemple :

jfc.addChoosableFileFilter(new javax.swing.filechooser.FileFilter()
    {
        @Override
        public boolean accept(File f)
        {
            return f.getName().toLowerCase().endsWith(".wiki");
        }
        @Override
        public String getDescription()
        {
            return "Wiki source file"; // Description utilisée dans la liste de choix.
        }
    });

Vous pouvez utiliser la sous-classe javax.swing.filechooser.FileNameExtensionFilter pour les filtres basés sur l'extension de fichier. Le constructeur prend en paramètre la description du filtre suivie de la liste des extensions de nom des fichiers acceptés :

FileFilter file_filter_image = new FileNameExtensionFilter("Image d'illustration", "png","bmp","jpeg","jpg","gif");

Ajouter une prévisualisation

modifier

La fenêtre de sélection de fichiers peut afficher un composant accessoire pour, par exemple, afficher un aperçu du fichier sélectionné (image, ...). Pour ajouter un tel composant, il faut utiliser la méthode setAccessory(Component newAccessory).

Exemple : Une vue de prévisualisation d'image :

import java.awt.*;
import java.beans.*;
import java.io.*;

import javax.imageio.*;
import javax.swing.*;

public class FilePreview extends JPanel
implements PropertyChangeListener
{
    // Largeur maximale de prévisualisation :
    private static final int PREF_IMAGE_WIDTH = 150;
    // Marge en pixels :
    private static final int MARGIN = 5;

    public FilePreview()
    {
        setPreferredSize(new Dimension(PREF_IMAGE_WIDTH+MARGIN, -1));
    }

    // Méthode de l'interface PropertyChangeListener appelée
    // quand une propriété est modifiée, pour mettre à jour
    // la prévisualisation quand le fichier sélectionné change.
    public void propertyChange(PropertyChangeEvent e)
    {
	    String property_name = e.getPropertyName();
        if (property_name.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY))
        {
            File selection = (File)e.getNewValue();
            updateView(selection == null ? null : selection.getAbsolutePath());
        }
    }

    private int width, height, xi,yi;
    private Image image, scaled_image=null;

    public static Image getImage(String p)
    {
        if (p==null) return null;
        return getImage(new File(p));
    }

    public static Image getImage(File fpath)
    {
        if (fpath==null) return null;
        try { return ImageIO.read(fpath); }
        catch (IOException e) { } // Ignorer l'erreur, ne pas afficher d'image.
        return null;
    }

    protected void updateView(String name)
	{
        info = DEFAULT_INFO;
        image = getImage(name);
        if (image == null && name!=null) info = "Le fichier sélectionné n'est pas une image";
        scaled_image = null;
        repaint();
    }

    @Override
    public void setBounds(int x, int y, int width, int height)
    {
        scaled_image = null;
        super.setBounds(x, y, width, height);
    }

    private static final String DEFAULT_INFO = "Pas d'image sélectionnée";
    private String info = DEFAULT_INFO; // Information sur l'image

    private void scaleImage(Dimension dd)
    {
        xi = MARGIN; yi = 0; // Position du cadre virtuel de l'image
        Dimension d = new Dimension(dd.width-MARGIN, dd.height-20);
        width = image.getWidth(this);
        height = image.getHeight(this);
        info = (width<0||height<0)?DEFAULT_INFO:""+width+"x"+height+" pixels";

        // Mise à l'échelle de l'image si besoin
        // et centrage dans le cadre virtuel :
        if ((d.width==width)&&(d.height==height))
        {
            scaled_image = image;
        }
        else if (width*d.height >= height*d.width)
        {
            height = height * d.width / width;
            width = d.width;
            scaled_image = image.getScaledInstance(width, height, Image.SCALE_SMOOTH);
            yi+=(d.height-height)/2;
        }
        else
        {
            width = width * d.height / height;
            height = d.height;
            scaled_image = image.getScaledInstance(width, height, Image.SCALE_SMOOTH);
            xi+=(d.width-width)/2;
        }
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        Dimension d = getSize(); if (d==null) return;
        if ((scaled_image == null)&&(image!=null)) scaleImage(d);
        g.setColor(getBackground());
        g.fillRect(0, 0, d.width, d.height);
        if (scaled_image!=null)
        {
            g.drawImage(scaled_image, xi, yi, this);
        }
        g.setColor(Color.BLACK);
        g.drawString(info, MARGIN, d.height-5);
    }
}

Une instance de cette classe s'ajoute en appelant les deux méthodes ci-dessous :

FilePreview fp = new FilePreview();
jfc.setAccessory(fp);              // Pour l'afficher
jfc.addPropertyChangeListener(fp); // Pour écouter le changement de fichier sélectionné


Menus

Types de menu

modifier

Swing supporte deux types de menu :

  • les menus de type javax.swing.JMenu attaché à une fenêtre via sa barre de menu javax.swing.JMenuBar généralement située en haut de la fenêtre.
  • les menus de type javax.swing.JPopupMenu attaché à tout composant, dont l'affichage est, par exemple, déclenché par un clic droit de la souris.

Création d'un menu

modifier

Ces deux types de menu (javax.swing.JMenu et javax.swing.JPopupMenu) s'utilisent de la même façon. Un menu est initialement vide, et l'ajout d'un item se fait en appelant l'une des méthodes suivantes :

add(JMenuItem item)
Ajouter un item de menu.
add(Action action)
Ajouter un item de menu créé à partir d'une action.

La seconde méthode est préférable car une même action peut être attachée à différents composants (menu, bouton, ...) ce qui simplifie la gestion de son état (état activé ou désactivé, texte, icône, raccourci clavier, ...) dont la modification de répercute sur tous les composants liés.

Il est possible d'ajouter d'autres éléments à un menu :

  • La méthode addSeparator() permet d'ajouter un séparateur entre deux items de menu.
  • La méthode add(JMenuItem item) accepte aussi un autre menu afin d'ajouter un sous-menu, car la classe javax.swing.JMenu dérive de javax.swing.JMenuItem.

Exemple :

package org.wikibooks.fr.swing;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

/**
 * Créer une fenêtre avec un menu.
 * @author fr.wikibooks.org
 */
public class ExempleMenuDeFenetre extends JFrame
{
	private void menuOuvrir()
	{
		// Mettre ici le code pour ouvrir un fichier
	}

	private void menuEnregistrer()
	{
		// Mettre ici le code pour sauvegarder un fichier
	}

	public ExempleMenuDeFenetre()
	{
		// Création de la fenêtre
		setTitle("Démonstration de création de menu pour une fenêtre Swing");
		Dimension d = new Dimension(500, 200);
		setMinimumSize(d);
		setSize(d);

		// Création des actions
		Action a_ouvrir = new AbstractAction("Ouvrir un fichier...")
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				menuOuvrir();
			}
		};
		a_ouvrir.setAccelerator(KeyStroke.getKeyStroke(
			KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));      // Ctrl-O

		Action a_enregistrer = new AbstractAction("Enregistrer le fichier...")
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				menuEnregistrer();
			}
		};
		a_enregistrer.setAccelerator(KeyStroke.getKeyStroke(
			KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));      // Ctrl-S

		// Création du menu
		JMenuBar mb = new JMenuBar();

		JMenu m = new JMenu("Fichier");  // Fichier :
		mb.add(m);

		m.add(new JMenuItem(a_ouvrir));      //  - Ouvrir
		m.addSeparator();                    //  ----------------
		m.add(new JMenuItem(a_enregistrer)); //  - Enregistrer

		setJMenuBar(mb); // Attacher la barre de menu à la fenêtre
	}

	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				ExempleMenuDeFenetre frame = new ExempleMenuDeFenetre();
				frame.setVisible(true);
			}
		});
	}
}

Gestion d'un menu contextuel

modifier

Un menu contextuel (classe javax.swing.JPopupMenu) a la particularité de s'afficher à l'endroit où l'utilisateur a déclenché son affichage (Par exemple, clic droit avec la souris sous Windows ou Linux, ou la touche Menu).

Le déclenchement

modifier

Le déclenchement de l'ouverture du menu dépend de la plateforme : il peut se faire lors de l'appui du bouton de souris, ou lors de son relâchement (comme sous Windows par exemple). La classe MouseEvent possède une méthode isPopupTrigger() permettant de tester si l'évènement est celui qui déclenche l'ouverture du menu pour la plateforme.

L'affichage du menu contextuel doit se faire avec la méthode show(JComponent, int, int) pour que le comportement du menu soit correct : mise en évidence de l'item survolé, gestion de la disparition du menu en cliquant ailleurs, ... L'utilisation des méthodes setLocation et setVisible n'est pas recommandée car le menu peut ne pas fonctionner correctement.

public class UnComposant extends JComponent
{
	// ...
	private JPopupMenu menu_contextuel;

	private boolean ouvrirMenuContextuel(MouseEvent e)
	{
		if (e.isPopupTrigger())
		{
			// L'ajustement du menu contextuel est possible ici si besoin, avant l'affichage.
			menu_contextuel.show(this, e.getX(), e.getY());
			return true; // menu contextuel ouvert
		}
		return false; // pas d'ouverture pour cet évènement
	}

	public UnComposant()
	{
		// ...
		menu_contextuel = new JPopupMenu();
		// ... ajouter des items au menu ici ...

		addMouseListener(new MouseAdapter()
		{
			@Override
			public void mouseReleased(MouseEvent e)
			{
				// si l'évènement n'est pas pour ouvrir le menu contextuel...
				if (!ouvrirMenuContextuel(e))
				{
					// ... alors faire une autre action
				}
			}

			@Override
			public void mousePressed(MouseEvent e)
			{
				// si l'évènement n'est pas pour ouvrir le menu contextuel...
				if (!ouvrirMenuContextuel(e))
				{
					// ... alors faire une autre action
				}
			}
		});
	}
}


Actions

Une action permet de préconfigurer plusieurs propriétés. Quand un composant (JRadiobutton, JButton, JMenuItem, ...) est lié à une action, ses propriétés suivent l'évolution de celles de l'action. Notamment, cela permet de désactiver/activer une action, de configurer un listener d'action (ActionListener) une seule fois. Cela est pratique quand une même action est disponible à la fois dans un menu et par un bouton, par exemple.

Exemple pour une action de menu, ou de bouton :

// Création d'une action
// Convention : a_ pour action
Action a_ouvrir = new AbstractAction("Ouvrir un fichier...")
{
	@Override
	public void actionPerformed(ActionEvent e)
	{
		menuOuvrir();
	}
};
a_ouvrir.setAccelerator(KeyStroke.getKeyStroke(
	KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));      // Ctrl-O

// Action ajoutée au menu fichier :
JMenu m = new JMenu("Fichier");  // Fichier :
m.add(new JMenuItem(a_ouvrir));      //  - Ouvrir

// Et aussi pour un bouton directement dans la fenêtre :
JButton b_ouvrir = new JButton(a_ouvrir);

a_ouvrir.setEnabled(false); // --> désactive à la fois le menu et le bouton en un appel


Types de données et constantes

Les interfaces graphiques en Swing utilisent les mêmes types de données qu'en AWT. Ces types sont utilisés abondamment dans l'API pour définir des informations graphiques :

  • Couleur
  • Dimensions
  • Police de caractères
  • Position
  • Orientation
  • Curseur de souris

Couleurs

modifier

Un objet de classe java.awt.Color représente une couleur au format RVBA (Rouge, Vert, Bleu et Alpha) à quatre composantes dont la valeur est entre 0 et 255. La composante alpha définit l'opacité de la couleur : 0 = transparente, 255 = opaque (valeur par défaut).

Le constructeur de la classe java.awt.Color permet de spécifier la couleur avec les trois composantes et éventuellement la valeur alpha pour la transparence, de type int entre 0 et 255 ou de type float entre 0.0 et 1.0.

Exemples :

public static final Color
	C_BLEU_GRIS = new Color(150,150,180), // alpha=255 pour 100% d'opacité par défaut
	C_BLEU_VERT = new Color(0,170,220, 192); // alpha=192 pour 75% d'opacité

La classe java.awt.Color définit quelques couleurs utilisées fréquemment :

BLUE(0,0,255) GREEN(0,255,0) RED(255,0,0) ORANGE(255,200,0)
YELLOW(255,255,0) MAGENTA(255,0,255) CYAN(0,255,255) PINK(255,175,175)
BLACK(0,0,0) DARK_GRAY(64,64,64) GRAY(128,128,128) LIGHT_GRAY(192,192,192) WHITE(255,255,255)

Police de caractères

modifier

Un objet de classe java.awt.Font représente la police de caractère utilisée pour dessiner le texte. La classe possède deux constructeurs :

Font(String name, int style, int size)
Les attributs sont définis par les arguments suivants :
  • name : Nom de la police de caractères. Il peut aussi s'agir d'un nom symbolique défini par Java disponible sur toutes les plateformes : Dialog, DialogInput, Monospaced, ...
  • style : Style de police, parmi les constantes définies par la classe : PLAIN, BOLD, ITALIC, BOLD_ITALIC.
  • size : Taille de la police de caractères en points.
Font(Map<? extends Attributes, ?> attributes)
Les attributs sont définis par un dictionnaire (Map) dont les clés doivent être celles définies par la classe java.awt.font.TextAttribute. Ce constructeur permet de définir davantage d'attributs.

 

La taille des polices de caractères en Java est en points pour 72 DPI (Dot Per Inch), quel que soit le DPI du système.

S'il est configuré convenablement au niveau système, le nombre de pixels par pouce (DPI) permet d'avoir un affichage de taille physique constant quel que soit le moyen d'affichage utilisé. Sachant qu'un point (pt) vaut 1/72 pouce (1/72 in), une police de caractère de 16pt devrait donc être affichée sur une hauteur de 21.333 pixels environ sur un écran à 96 DPI[1] :

 

Cependant, Java utilise 72 DPI, ce qui signifie que la taille donnée en point sera la même en pixels : 16pt affiché sur 16 pixels. Les polices de caractères apparaissent donc plus petites dans les applications Java par rapport aux autres applications (traitement de texte, logiciel de dessin, navigateur...) sur les écrans ayant un DPI supérieur et plus grandes sur les écrans avec un DPI inférieur à 72.

Pour obtenir un affichage identique aux autres applications, en ajoutant un paramètre dans l'application pour configurer le nombre de points par pouce, il faut ajuster la taille de la police. Pour l'exemple d'une police de caractères de 16 points sur un écran de 96 DPI, il faut utiliser une taille de 21,333. Cependant, le constructeur acceptant un nombre à virgule flottante pour la taille au lieu d'un entier est privé, mais la méthode deriveFont autorise un nombre à virgule flottante pour la taille.

int dpi = 96;
static final int dpi_java = 72;
// La taille initiale (entier) passée au constructeur n'a pas d'importance.
Font f_16_points = new Font("Arial", Font.PLAIN, 16).deriveFont(16f*dpi/dpi_java);

Cependant, au lieu d'utiliser une valeur fixe, il est préférable d'utiliser la valeur retournée par la méthode getScreenResolution() de la classe java.awt.Toolkit :

int dpi = java.awt.Toolkit.getDefaultToolkit().getScreenResolution();
static final int dpi_java = 72;
Font f_16_points = new Font("Arial", Font.PLAIN, 16).deriveFont(16f*dpi/dpi_java);

Le nombre de points par pouce n'est toutefois pas toujours bien configuré. La solution est de prévoir un paramètre de configuration pour changer la valeur de densité, en utilisant la valeur retournée par la méthode getScreenResolution() comme valeur par défaut.

Dimensions

modifier

Un objet de classe java.awt.Dimension définit la taille d'un composant avec deux attributs publics, qui peuvent être initialisés en passant leur valeur en argument du constructeur :

int width
Largeur en pixels (0 par défaut).
int height
Hauteur en pixels (0 par défaut).

Position

modifier

Un objet de classe java.awt.Point définit la position d'un composant avec deux attributs publics, qui peuvent être initialisés en passant leur valeur en argument du constructeur :

int x
Position horizontale en pixels (0 par défaut), relative au bord gauche du conteneur.
int y
Position vertical en pixels (0 par défaut), relative au bord supérieur du conteneur.

Rectangle

modifier

Un objet de classe java.awt.Rectangle définit à la fois la la position d'un composant et sa taille avec quatre attributs publics, qui peuvent être initialisés en passant leur valeur en argument du constructeur :

int x
Position horizontale en pixels (0 par défaut), relative au bord gauche du conteneur.
int y
Position vertical en pixels (0 par défaut), relative au bord supérieur du conteneur.
int width
Largeur en pixels (0 par défaut).
int height
Hauteur en pixels (0 par défaut).

La classe Rectangle a aussi des constructeurs acceptant une position (java.awt.Point) ou une taille (java.awt.Dimension) ou les deux.

Rectangle zone = new Rectangle(20, 10, 300, 200); // position x=20, y=10 ; taille width=300, height=200

Point position = new Point(20, 10);
Dimension taille = new Dimension(300, 200);
Rectangle zone = new Rectangle(position, taille);

Cette classe permet aussi quelques opérations :

Rectangle zone1 = new Rectangle(20, 10, 300, 200); // position x=20, y=10 ; taille width=300, height=200
Rectangle zone2 = new Rectangle(40, 50, 320, 250); // position x=40, y=50 ; taille width=320, height=250

Rectangle zone_englobante = zone1.union(zone2);
Rectangle zone_commune = zone1.intersection(zone2);

Constantes de position et orientation

modifier

L'interface javax.swing.SwingConstants est une collection de constantes relatives à la position et l'orientation. Les tableaux ci-dessous montrent leur valeur et ce qu'elle représente.

Le centre a la valeur nulle, et les autres constantes de positionnement sont distribuées ensuite dans le sens trigonométrique (anti-horaire) en partant du haut.

TOP = 1
LEFT = 2 CENTER = 0 RIGHT = 4
BOTTOM = 3

Le centre a la valeur nulle, et les autres constantes de direction sont distribuées ensuite dans le sens horaire (anti-trigonométrique) en partant du haut.

NORTH_WEST = 8 NORTH = 1 NORTH_EAST = 2
WEST = 7 CENTER = 0 EAST = 3
SOUTH_WEST = 6 SOUTH = 5 SOUTH_EAST = 4

Les autres constantes définies sont les constantes d'orientation :

  • HORIZONTAL = 0
  • VERTICAL = 1

les constantes de position selon l'orientation de lecture de la langue courante :

  • LEADING = 10 avant dans le sens de lecture.
Cela correspond à gauche (respectivement droite) si la langue courante se lit de gauche à droite (respectivement de droite à gauche),
  • TRAILING = 11 après dans le sens de lecture.
Cela correspond à droite (respectivement gauche) si la langue courante se lit de gauche à droite (respectivement de droite à gauche).

et les constantes de direction dans une séquence :

  • NEXT = 12
  • PREVIOUS = 13

Curseur de souris

modifier

Un objet de classe java.awt.Cursor définit le curseur utilisé quand la souris survole un composant. Le curseur par défaut est défini en fonction du composant : une flèche par défaut, une ligne verticale pour un champ textuel, ... La méthode setCursor(Cursor c) est définie pour toutes les classes de composants et fenêtres.

Afin de conserver une cohérence avec les autres applications, le changement de curseur n'est conseillé que pour certains cas exceptionnels et la création de nouveaux composants.

Plusieurs méthodes statiques permettent d'utiliser un curseur prédéfini :

Cursor.getPredefinedCursor(int type)
Retourne un curseur typique (disponible sur toutes les plateformes Java) parmi ceux-ci :
  • Cursor.DEFAULT_CURSOR Curseur par défaut (flèche),
  • Cursor.CROSSHAIR_CURSOR Curseur en croix (ligne verticale + ligne horizontale),
  • Cursor.TEXT_CURSOR Curseur pour les champs de texte,
  • Cursor.WAIT_CURSOR Curseur d'attente durant une action en cours (sablier, ...), peut être animé selon la plateforme,
  • Cursor.direction_RESIZE_CURSOR Groupe de curseurs de la souris de type flèche directionnelle ou bidirectionnelle utilisé généralement pour le redimensionnement des fenêtres, quand la souris approche du bord : (N=North (nord), S=South (sud), E=East (est), W=West (ouest)) SW_RESIZE_CURSOR, SE_RESIZE_CURSOR, NW_RESIZE_CURSOR, NE_RESIZE_CURSOR, S_RESIZE_CURSOR, N_RESIZE_CURSOR, W_RESIZE_CURSOR, E_RESIZE_CURSOR,
  • Cursor.MOVE_CURSOR Curseur de type flèche dans les 4 directions cardinales, généralement utilisé lors du déplacement d'un élément,
  • Cursor.HAND_CURSOR Curseur de type main, généralement utilisé pour les actions ouvrant une fenêtre ou une page (lien) ;
Cursor.getDefaultCursor()
Retourne le curseur par défaut (flèche) ;
Équivaut à : Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
Cursor.getSystemCustomCursor(String name)
Retourne un curseur spécifique au système, à partir d'un nom (par exemple "Invalid.32x32").
Ces curseurs sont définis dans le fichier de configuration cursors.properties du JRE (ou le sous-répertoire "jre" du JDK) localisé dans le répertoire lib/images/cursors. Les fichiers images référencés sont situés dans le même répertoire.
Exemple pour JDK 1.8 sous Windows, extrait du fichier C:\Program Files\Java\jdk1.8.0_05\jre\lib\images\cursors\cursors.properties :
#
Cursor.LinkNoDrop.32x32.File=win32_LinkNoDrop32x32.gif
Cursor.LinkNoDrop.32x32.HotSpot=6,2
Cursor.LinkNoDrop.32x32.Name=LinkNoDrop32x32
#
Cursor.Invalid.32x32.File=invalid32x32.gif
Cursor.Invalid.32x32.HotSpot=6,2
Cursor.Invalid.32x32.Name=Invalid32x32

Il est également possible de créer un curseur personnalisé à partir d'une image et d'un point dans l'image. Ce point détermine quel pixel de l'image correspond à la position de la souris.

Toolkit toolkit = Toolkit.getDefaultToolkit();
Cursor c = toolkit.createCustomCursor(image, new Point(x,y), nom_du_curseur);

Notes de bas de page

modifier
  1. Exemple d'un écran de 23 pouces en résolution 1980x1080 (diagonale approximative de 2229.12 pixels) :   pixels par pouce.


Gestion de la disposition des composants

La disposition des composants n'est pas définie directement par le conteneur mais par le gestionnaire de disposition des composants (Layout manager en anglais).

Informations utilisées et contrainte associée

modifier

Le gestionnaire peut utiliser différentes informations fournies par le composant :

Dimension getPreferredSize()
La taille préférée du composant, qui dépend en général de son contenu et du moteur de rendu utilisé.
Dimension getMinimumSize()
La taille minimale du composant. C'est la taille en dessous de laquelle le composant est jugé non utilisable (pas assez de place pour tout afficher, ...).
Dimension getMaximumSize()
La taille maximale du composant.

Les gestionnaires de disposition ne respectent pas tous les trois contraintes précédentes définies par le composant.

Le code typique de création d'un conteneur avec gestionnaire de disposition est le suivant :

// Créer le conteneur et lui associer le gestionnaire de disposition Conteneur c = new Conteneur() c.setLayout(new GestionnaireDeDisposition(arguments...));

// Ajouter les composants avec une contrainte de disposition c.add(composant, contrainte);

Exemple :

JPanel p_main = new JPanel();
p_main.setLayout(new BorderLayout());

// Titre en haut (au nord)
JLabel label_titre = new JLabel("Titre ici");
p_main.add(label_titre, BorderLayout.NORTH);

// Panel de vue des données au centre
JPanel p_view = new JPanel();
p_main.add(p_view, BorderLayout.CENTER);

// Panel des boutons en bas (au sud)
JPanel p_boutons = new JPanel();
p_main.add(p_boutons, BorderLayout.SOUTH);

Gestionnaires de disposition prédéfinis

modifier

Les interfaces java.awt.LayoutManager et java.awt.LayoutManager2 sont définies par une variété de classes définissant différentes façon de disposer les composants :

FlowLayout
Les composants sont dimensionnés à leur taille préférée, et disposés en ligne, avec passage à la ligne suivante dès que la largeur restante ne suffit plus. Les lignes peuvent être justifiées à gauche, à droite ou centrées dans le contenant.
 
Exemple de boutons alignés avec FlowLayout.
new FlowLayout() // composants alignés à gauche par défaut
new FlowLayout(FlowLayout.LEFT) // composants alignés à gauche
new FlowLayout(FlowLayout.RIGHT) // composants alignés à droite
new FlowLayout(FlowLayout.CENTER) // composants centrés
L'alignement peut aussi prendre en compte le sens de lecture de gauche à droite ou de droite à gauche, leading pour le début (gauche / droite) et trailing pour la fin (droite / gauche) :
new FlowLayout(FlowLayout.LEADING) // composants alignés en début de ligne
new FlowLayout(FlowLayout.TRAILING) // composants alignés en fin de ligne
Le constructeur peut prendre deux paramètres supplémentaires, espacement horizontal et vertical, pour spécifier l'espacement entre les composants et entre le conteneur et les composants :
new FlowLayout(FlowLayout.RIGHT, 10, 5) // composants alignés à droite espacés de 10 pixels en horizontal et 5 en vertical.
BorderLayout
Le conteneur est divisé verticalement en trois zones, de haut en bas : nord (BorderLayout.NORTH), centrale et sud (BorderLayout.SOUTH). La partie centrale est elle-même divisée horizontalement en trois zones, de gauche à droite : ouest (BorderLayout.WEST), centre (BorderLayout.CENTER) et est (BorderLayout.EAST).
 
Le constructeur peut prendre deux paramètres, espacement horizontal et vertical, pour spécifier l'espacement entre les composants :
new BorderLayout() // composants non espacés
new BorderLayout(10, 5) // composants espacés de 10 pixels en horizontal et 5 en vertical.
GridLayout
Les composants sont dimensionnés selon la taille du conteneur divisée en colonnes et lignes dont le nombre et l'espacement est indiqué à l'appel du constructeur. Les composants sont positionnés dans l'ordre de leur ajout au conteneur, de gauche à droite en partant de la ligne supérieure.
GridBagLayout
Les composants sont dimensionnés à leur taille préférée si la contrainte associée le permet. La contrainte associée est de classe GridBagConstraint et définit la position dans la grille, le nombre de cellules occupées, et les contraintes de taille et d'alignement du composant dans la cellule.
CardLayout
Les composants sont dimensionnés à la taille du conteneur, et un seul des composants n'est visible à la fois. Cette disposition est utilisée pour implémenter les onglets.
GroupLayout
Les composants sont dimensionnés à leur taille préférée si possible, et groupés ensemble soit parallèlement, soit séquentiellement, en vertical et en horizontal. Cela signifie que les composants sont ajoutés deux fois par le gestionnaire. Ils ne sont pas ajoutés directement au conteneur.
BoxLayout
Les composants sont dimensionnés à leur taille préférée si possible, et alignés verticalement ou horizontalement. L'alignement selon l'autre axe est définie par l'alignement préférée du composant. Ce gestionnaire tient compte également de la taille minimale et maximale de chaque composant.

Le gestionnaire de disposition doit être assigné au conteneur soit en le passant au constructeur, soit en appelant la méthode setLayout après construction du conteneur. Exemple:

JPanel p_main = new JPanel(new BorderLayout());

Pour BoxLayout, l'utilisation de la méthode setLayout est obligatoire car le constructeur a besoin de l'instance du conteneur :

JPanel p_main = new JPanel();
p_main.setLayout(new BoxLayout(p_main, BoxLayout.Y_AXIS));
// L'appel à setLayout doit se faire avec le même conteneur que celui passé à BoxLayout
// sinon l'exception suivante est levée :
//     java.awt.AWTError: BoxLayout can't be shared

Disposition par défaut des conteneurs

modifier

Un conteneur possède un gestionnaire de disposition par défaut.

JPanel
FlowLayout est le gestionnaire de disposition par défaut.
JTabbedPane
CardLayout est le gestionnaire de disposition utilisé car ce conteneur est une vue à onglets. Les onglets sont visibles en haut du conteneur par défaut, et cliquable par l'utilisateur pour changer l'onglet visible.


Gestion des évènements

Un composant peut recevoir divers évènements issus de l'interaction avec l'utilisateur :

  • Avec la souris : enfoncement et relâchement d'un bouton, déplacement de la souris, défilement de la molette.
  • Avec le clavier : enfoncement et relâchement d'une touche, génération d'un caractère.
  • Avec d'autres périphériques d'entrée.

Il peut aussi en générer :

  • Un bouton peut notifier le déclenchement d'une action.
  • La sélection d'un item dans une liste peut déclencher un évènement.
  • ...

Principes de gestion des évènements

modifier

De manière générale, la gestion des évènements en Java se fait de la manière suivante :

  • Les classes voulant recevoir une notification d'un évènement enregistrent un écouteur auprès de la source d'évènements. L'écouteur est un objet dont la classe implémente une interface particulière pour traiter un évènement comportant en général une seule méthode appelée pour notifier l'évènement qui s'est produit.
  • La source d'évènement (un composant, un périphérique d'entrée : souris ou clavier, ...) notifie les écouteurs enregistrés en appelant la méthode de l'interface particulière. Cette méthode ne comporte en général qu'un seul argument dont la classe dérive de la classe java.util.EventObject.

L'exemple ci-dessous illustre comment recevoir un évènement d'action sur un bouton, qui se déclenche au clic avec la souris, ou au clavier quand le bouton a le focus.

JButton b_ok = new JButton("OK");
b_ok.addActionListener(new ActionListener()
{
	@Override
	public void actionPerformed(ActionEvent e)
	{
		// ... Action du bouton OK ici
		System.out.println("Clic du bouton OK");
	}
});

La convention de gestion des évènements est basée sur celle des JavaBeans. Pour un évènement de type type (MouseWheel, Key, Mouse, ...) :

  • Une classe d'évènement typeEvent dérivant de la classe java.util.EventObject contient toutes les informations sur l'évènement notifié.
  • Un écouteur doit implémenter l'interface typeListener dont la ou les méthode(s) ont comme seul argument un objet de classe typeEvent.
  • La source d'évènement possède deux méthodes publiques :
    • addtypeListener pour ajouter un écouteur de l'évènement.
    • removetypeListener pour retirer un écouteur.
  • En général, en interne, la source d'évènement notifie les écouteurs enregistrés avec une méthode privée ou protégée nommée firetypeEvent.

Les différents types d'évènements

modifier

Il existe un grand nombre de types d'évènements représentés par des classes dérivant de la classe java.util.EventObject. Cette section ne citera que les principaux types parmi ceux concernant les interfaces graphiques avec Swing.

La hiérarchie des types d'évènements contient un certain nombre de classes, chacune pouvant ajouter une ou plusieurs informations sur l'évènement.

Classe Information(s) sur l'évènement définie(s)/ajoutée(s) par la classe
java.util.EventObject Source de l'évènement : public Object getSource();
└─ java.awt.AWTEvent Identification de l'évènement (ID), consommation de l'évènement
├─ java.awt.event.ActionEvent Heure de l'évènement et modificateurs (boutons de souris, touches enfoncées : Shift, Alt, Ctrl...)
(ID: ACTION_PERFORMED)
└─ java.awt.event.ComponentEvent Composant source de l'évènement (conversion de type)
└─ java.awt.event.InputEvent Heure de l'évènement et modificateurs (boutons de souris, touches enfoncées : Shift, Alt, Ctrl...)
├─ java.awt.event.KeyEvent Code de la touche, caractère généré
(ID: KEY_TYPED, KEY_PRESSED, KEY_RELEASED)
└─ java.awt.event.MouseEvent Position de la souris, nombre de clics, indicateur de menu popup
(ID: MOUSE_CLICKED, MOUSE_PRESSED, MOUSE_RELEASED, MOUSE_MOVED, MOUSE_ENTERED, MOUSE_EXITED, MOUSE_DRAGGED, MOUSE_WHEEL)
└─ java.awt.event.MouseWheelEvent Rotation de la molette de la souris (nombre, pixels de défilement)

Les sous-classes de la classe java.awt.AWTEvent ont un attribut (ID) qui identifie une notification particulière d'un évènement, chacune correspondant à une méthode de l'interface d'écoute de l'évènement. Par exemple, pour les évènements provenant de la souris, les méthodes de l'interface java.awt.event.MouseListener sont appelées avec un objet évènement de classe java.awt.event.MouseEvent dont la valeur de l'identificateur (ID) correspond à celles-ci :

public void mouseReleased(MouseEvent e); // e.getID() == MouseEvent.MOUSE_RELEASED
public void mousePressed(MouseEvent e);  // e.getID() == MouseEvent.MOUSE_PRESSED
public void mouseExited(MouseEvent e);   // e.getID() == MouseEvent.MOUSE_EXITED
public void mouseEntered(MouseEvent e);  // e.getID() == MouseEvent.MOUSE_ENTERED
public void mouseClicked(MouseEvent e);  // e.getID() == MouseEvent.MOUSE_CLICKED

Même principe pour les méthodes de l'interface java.awt.event.MouseMotionListener pour intercepter les évènements de mouvement de la souris :

public void mouseMoved(MouseEvent e);   // e.getID() == MouseEvent.MOUSE_MOVED    (Souris déplacée sans bouton enfoncé)
public void mouseDragged(MouseEvent e); // e.getID() == MouseEvent.MOUSE_DRAGGED  (Souris déplacée avec bouton enfoncé)

Cela s'applique aussi aux interfaces qui n'ont qu'une seule méthode, comme l'interface java.awt.event.MouseWheelListener pour la notification de rotation de molette de la souris :

public void mouseWheelMoved(MouseWheelEvent e); // e.getID() == MouseEvent.MOUSE_WHEEL

Cette redondance apparente d'identification du type d'évènement (par la méthode appelée et par la valeur de l'attribut ID) facilite la réutilisation d'une même méthode pour traiter plusieurs évènements de façon similaire.

Implémentation partielle

modifier

Les interfaces d'écoute définissant au moins deux méthodes possèdent une classe dont le nom est celui de l'interface où l'on remplace le suffixe Listener par Adapter. Cette classe définit des méthodes vides et permet aux applications de la surcharger pour ne définir que certaines méthodes de l'interface.

Exemple :

addWindowListener(new WindowAdapter()
{
	@Override
	public void windowClosing(WindowEvent e)
	{
		fermerFenetre();
	}
});


Gestion des actions asynchrones

La création d'une interface graphique pour une application en Java entraîne la création de threads pour gérer les tâches de fond comme la gestion des évènements qui créer une boucle de réception d'évènements et leur distribution aux composants visibles.

Les actions déclenchées par l'utilisateur peuvent prendre beaucoup de temps : recherche de fichiers, calculs complexes, ... Lorsque de telles actions sont appelées directement depuis les écouteurs d'évènements, l'interface graphique est bloquée durant les secondes ou minutes d'exécution. Pour éviter ce problème, il faut gérer correctement les appels à ces actions de manière asynchrone.

Action asynchrone

modifier

Pour éviter qu'une action dont l'exécution est longue empêche l'utilisateur d'utiliser l'interface graphique, il faut créer un thread spécifique.

Exemple : Une action longue exécutée par clic sur un bouton :

JButton b_action = new JButton("Démarrer l'action")
b_action.addActionListener(new ActionListener()
	{
		public void actionPerformed(ActionEvent e) { demarrerAction(); }
	});

La méthode demarrerAction() crée et démarre un thread pour exécuter l'action et retourne sans attendre la fin d'exécution afin de ne pas bloquer l'interface graphique :

private void demarrerAction()
{
	new Thread(run_action_longue, "longue-action").start();
}

// L'action du thread : appeler la méthode actionLongue()
Runnable run_action_longue = new Runnable()
{
	@Override
	public void run()
	{ actionLongue(); }
};

private void actionLongue()
{
	// Ici l'action longue...

	// Pour tester, attendre 10 secondes :
	try{ Thread.sleep(10000); }
	catch(InterruptedException ex){ }
}

Mise à jour de l'interface

modifier

Une action asynchrone peut avoir besoin de mettre à jour l'interface graphique : indiquer que l'action est terminée, afficher le fichier ou l'image chargée ou le résultat d'un calcul, ... Pour cela, les méthodes des composants doivent être appelées par le thread de gestion des évènements afin d'éviter les conflits. Il suffit pour cela d'appeler la méthode statique invokeLater de la classe javax.swing.SwingUtilities.

private void actionLongue()
{
	// Ici l'action longue...
	// resultat = ...

	// Afficher le résultat :
	SwingUtilities.invokeLater(new Runnable()
	{
		@Override
		public void run()
		{ afficher(resultat); }
	});
}


Apparence de l'interface

L'apparence et le comportement (look and feel en anglais) des composants Swing d'une application sont modifiables et configurables par le gestionnaire d'apparence et de comportement. L'apparence et le comportement varient plus largement pour les boîtes de dialogue standards telle celle de l'ouverture de fichiers où les composants utilisés diffèrent :

 

Apparence de l'application

modifier

Les composants Swing ont une apparence et un comportement (look and feel en anglais) modifiables définis par le gestionnaire d'apparence et de comportement. L'apparence peut ensuite être modifiée pour chaque composant en appelant les méthodes communes :

  • setBackground(Color) : Changer la couleur de fond.
  • setForeground(Color) : Changer la couleur de premier plan (texte, ligne en général).
  • setFont(Font) : Changer la police de caractères, si le composant affiche du texte.
  • setBorder(Border) : Changer la bordure autour du composant (aucune par défaut en général).

Les méthodes get correspondantes existent également permettant d'obtenir la valeur courante.

Le gestionnaire d'apparence et de comportement par défaut est Metal, disponible quel que soit la plateforme. La classe UIManager permet de changer l'apparence et le comportement en appelant la méthode setLookAndFeel et en lui passant le nom de la classe implémentant l'apparence et le comportement voulus.

Si l'application effectue un changement, il faut le faire avant la création des fenêtres concernées. Par exemple dans la méthode main :

protected static String laf_selected = null;

// Sélection par nom de classe
private static boolean selectClass(String classname)
{
	try
	{
		UIManager.setLookAndFeel(classname);
		laf_selected = classname;
		return true;
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
	return false;
}

// Sélection par nom d'affichage, recherche parmi les looks & feels disponibles.
public static boolean select(String name)
{
	for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels() )
	{
		if (name.equalsIgnoreCase(laf.getName()))
			return selectClass(laf.getClassName());
	}
	return false;
}

public static void main(String[] args)
{
	// Choisir l'apparence avant de créer une fenêtre
	select("Nimbus");
	//selectClass(UIManager.getSystemLookAndFeelClassName()); // Apparence de l'OS
	//selectClass(UIManager.getCrossPlatformLookAndFeelClassName()); // Apparence cross-plateforme (Metal)
	EventQueue.invokeLater(new Runnable()
	{
		public void run()
		{
			DemonstrationApparence frame = new DemonstrationApparence();
			frame.setVisible(true);
		}
	});
}

Si l'application effectue le changement après création des fenêtres, la mise à jour doit se faire explicitement en appelant la méthode statique SwingUtilities.updateComponentTreeUI pour chaque fenêtre à mettre à jour en argument.

Aperçu

modifier

Les copies d'écran ci-dessous montrent l'apparence de divers composants Swing selon l'apparence sélectionnée. Les apparences disponibles dépendent du système d'exploitation sous lequel tourne l'application Java. L'apparence peut modifier les couleurs, la police de caractère par défaut, la taille, les marges des différents composants. Par contre, l'apparence du bord de la fenêtre ne change pas.

Sous windows 7 :


 
Apparence Metal pour les bords d'une fenêtre Swing (JFrame)

L'aspect de tous les composants changent, mais celui du bord de la fenêtre ne change pas par défaut. Le changement de l'aspect du bord de fenêtre n'est pas supporté par toutes les apparences. La seule apparence à le supporter est le look&feel Metal. Pour activer le changement d'apparence de décoration des fenêtres et boîtes de dialogue, il faut appeler ces méthodes (les deux en général) :

JDialog.setDefaultLookAndFeelDecorated(true);
JFrame.setDefaultLookAndFeelDecorated(true);

Ces deux méthodes doivent être appelées avant la création des fenêtres et boîtes de dialogue. On peut obtenir le même effet sur une fenêtre individuelle :

JFrame frame = new JFrame();
frame.setUndecorated(true);
frame.getRootPane().setWindowDecorationStyle(JRootPane.FRAME);

Pour une boîte de dialogue :

JDialog dialog = new JDialog();
dialog.setUndecorated(true);
dialog.getRootPane().setWindowDecorationStyle(JRootPane.PLAIN_DIALOG);

La personnalisation de l'apparence des bords de fenêtres est effectivement géré par JRootPane, dans une fenêtre sans décoration. Si le changement d'apparence de décoration des fenêtres et boîtes de dialogue est activé avec une apparence qui ne le supporte pas, la fenêtre n'a plus de bord.

 
Apparence Metal (bords de fenêtre inclus), avec gras désactivé

L'apparence peut être configurée avant la création de fenêtre.

Par exemple, il est possible de désactiver les polices en gras de l'apparence Metal :

UIManager.put("swing.boldMetal", Boolean.FALSE);

Code source

modifier

Le code source ci-dessous est celui de l'application ayant permis les captures d'écrans de ce chapitre.

package org.wikibooks.fr.swing;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

/**
 * Démonstration du changement d'apparence. 
 * @author fr.wikibooks.org
 */
public class DemonstrationApparence extends JFrame
{
	private String[] laf_classnames;
	private String[] laf_names;
	private int index_cross_platform = -1;
	private int index_system = -1;
	private int index_selected = -1;

	private void changerApparence(int index)
	{
		if (index != index_selected)
		{
			index_selected = index;
			if (selectClass(laf_classnames[index]))
				SwingUtilities.updateComponentTreeUI(this);
			else
				JOptionPane.showMessageDialog(this,
					"Impossible de changer l'apparence à "+laf_names[index],
					"Erreur", JOptionPane.ERROR_MESSAGE);
		}
	}

	public DemonstrationApparence()
	{
		// Lister les apparences disponibles
		UIManager.LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
		laf_classnames = new String[lafs.length];
		laf_names = new String[lafs.length];
		String laf_system = UIManager.getSystemLookAndFeelClassName();
		String laf_cross_platform = UIManager.getCrossPlatformLookAndFeelClassName();
		index_selected = -1;
		for(int i=0 ; i<lafs.length ; i++)
		{
			UIManager.LookAndFeelInfo laf = lafs[i];
			String classname = laf.getClassName();
			laf_classnames[i] = classname;
			laf_names[i] = laf.getName();
			if (laf_selected!=null && laf_selected.equals(classname))
				index_selected = i;
			if (laf_system.equals(classname))
				index_system = i;
			if (laf_cross_platform.equals(classname))
			{
				index_cross_platform = i;
				if (laf_selected==null) index_selected = i;
			}
		}

		// Création de la fenêtre
		setTitle("Démonstration de l'apparence des composants Swing");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		Dimension d = new Dimension(450, 440);
		setMinimumSize(d);
		setSize(d);
		Container c = getContentPane();
		c.setLayout(new BorderLayout());

		JPanel p_main = new JPanel();
		p_main.setLayout(new BoxLayout(p_main, BoxLayout.Y_AXIS));
		p_main.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		c.add(p_main, BorderLayout.WEST);

		JLabel l_apparence = new JLabel("Choisissez l'apparence :");
		p_main.add(l_apparence);

		// Un seul bouton radio appartenant au groupe ne pourra être sélectionné
		ButtonGroup group = new ButtonGroup();
		JRadioButton rb_sel = null;
		for(int i=0 ; i<lafs.length ; i++)
		{
			String name = laf_names[i];
			if (i==index_cross_platform) name = name+" (Cross-platform)";
			if (i==index_system) name = name+" (System)";
			JRadioButton rb = new JRadioButton(name);
			p_main.add(rb);
			group.add(rb);
			if (i == index_selected) rb_sel = rb;
			final int rb_index = i;
			rb.addItemListener(new ItemListener()
			{
				@Override
				public void itemStateChanged(ItemEvent e)
				{
					if (rb.isSelected())
						changerApparence(rb_index);
				}
			});
		}
		if (rb_sel!=null) rb_sel.setSelected(true);

		JPanel p_comp = new JPanel();
		p_comp.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		p_comp.setLayout(new BoxLayout(p_comp, BoxLayout.Y_AXIS));
		c.add(p_comp, BorderLayout.CENTER);

		// Quelques composants pour montrer leur apparence
		// espacés de 10 pixels verticalement
		p_comp.add(new JCheckBox("Case à cocher"));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JSpinner(new SpinnerNumberModel(50, 0, 100, 1)));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JComboBox<String>(new String[]{"Choix 1", "Choix 2", "Choix 3"}));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JTextField("Champ de saisie"));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JScrollPane(new JTextArea("Zone de saisie\nSur plusieurs lignes\n1 2 3 4 5 6 7 8 9\n...")));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JButton("Un bouton"));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JToggleButton("Un bouton basculable"));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(new JScrollPane(new JTable(
			new Object[][]
				{
				{ "pi",  3.14159, true },
				{ "phi", 1.61803, false },
				{ "e",   2.71828, true },
				{ "square root of 2", 1.41421, false },
				{ "square root of 3", 1.73205, false },
				{ "square root of 5", 2.23607, false },
				{ "square root of 7", 2.64575, false },
				},
			new Object[]{ "Nom", "Valeur", "Utiliser" })));
		p_comp.add(Box.createVerticalStrut(10));
		p_comp.add(Box.createVerticalGlue());
	}

	protected static String laf_selected = null;

	private static boolean selectClass(String classname)
	{
		try
		{
			UIManager.setLookAndFeel(classname);
			laf_selected = classname;
			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		return false;
	}

	public static boolean select(String name)
	{
		for (UIManager.LookAndFeelInfo laf : UIManager.getInstalledLookAndFeels() )
		{
			if (name.equalsIgnoreCase(laf.getName()))
				return selectClass(laf.getClassName());
		}
		return false;
	}

	public static void main(String[] args)
	{
		// Choisir l'apparence avant de créer une fenêtre
		select("Nimbus");
		//selectClass(UIManager.getSystemLookAndFeelClassName());
		//selectClass(UIManager.getCrossPlatformLookAndFeelClassName());
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				DemonstrationApparence frame = new DemonstrationApparence();
				frame.setVisible(true);
			}
		});
	}
}


Contenu en HTML

La plupart des composants Swing supporte le texte en HTML. Cela permet d'ajouter facilement du texte formaté pour améliorer la présentation de l'interface graphique.

Affichage en HTML

modifier
JLabel, JButton
Pour un rendu HTML, le texte doit être encadré par <html> et </html>.
Exemple :
JLabel l_title = new JLabel("<html>Le titre avec <b>une partie en gras</b>,"+
    " et <span style=\"color:blue;\">une autre partie en bleu</span>.</html>");
JTextPane
En plus d'encadrer le texte par <html> et </html>, il faut spécifier le type de contenu, car ce composant supporte plusieurs types de document.
Exemple :
JTextPane tp_exemple = new JTextPane();
tp_exemple.setContentType("text/html");
tp_exemple.setText("<html><head><style>p { margin: 20px 0; }</style></head>"+
    "<body><h1>Titre</h1><p>Le texte avec <b>une partie en gras</b>,"+
    " et <span style=\"color:blue;\">une autre partie en bleu</span>.</p></body></html>");
Ce composant mémorise la feuille de style définie par l'entête (head) du source HTML.
Il faut forcer le changement de type pour la supprimer comme montré ci-dessous :
// Force la suppression de la feuille de style mémorisée :
tp_exemple.setContentType("text/plain");
// Utiliser les propriétés du composant comme style par défaut (couleurs, police de caractères) :
tp_exemple.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
tp_exemple.setContentType("text/html");
tp_exemple.setText("<html><h1>Autre titre</h1><p>Autre texte.</p></html>");

Limitations

modifier

Swing supporte HTML 4 et CSS 1.0 de manière limitée.

Limitations HTML

modifier
Éléments HTML supportés par Swing
Élément(s) Supporté(s)
<p> <span> <div> Oui
<b> <i> <u> <sup> <sub> Oui
<strong> <em> Oui
<br> <hr> Oui
<ins> <del> Oui, mais rendu spécifique
<pre> Non
Attributs supportés par Swing
Attribut(s) Supporté(s)
href src Oui
class style Oui

Limitations CSS

modifier
Attributs supportés
Attribut(s) Supporté(s)
color background Oui
font font-family Oui
border border-spacing Oui
border-top border-bottom border-left border-right border-collapse Non
margin padding Oui
margin-top margin-bottom margin-left margin-right Non
padding-top padding-bottom padding-left padding-right Non

Tester l'affichage en HTML

modifier

L'application dont le code source est ci-dessous permet de tester l'affichage en HTML avec un composant JTextPane.

Aperçu

modifier

 

Code source

modifier
package org.wikibooks.fr.swing.html;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;

/**
 * Fenêtre pour tester le rendu HTML en Swing avec un JTextPane.
 * @author fr.wikibooks.org
 */
public class FenetreRenduHtml extends JFrame
{
	private JPanel p_content;
	private JTextArea ta_css;
	private JTextArea ta_html_body;
	private JTextPane tp_html;

	private void renderHtml(String html)
	{
		tp_html.setContentType("text/plain");
		tp_html.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
		tp_html.setContentType("text/html");
		tp_html.setText("<html>"+html+"</html>");
	}

	private void renderHtml(String html_body, String style)
	{
		if (style==null) renderHtml(html_body);
		else renderHtml("<head><style>\n"+style+"\n</style></head><body>"+html_body+"</body>");
	}

	private void doHtmlRender()
	{
		String
			t_html_body = ta_html_body.getText(),
			t_css = ta_css.getText();
		renderHtml(t_html_body, t_css);
	}

	public FenetreRenduHtml()
	{
		setTitle("Test d'affichage en HTML");
		Dimension d = new Dimension(800, 600);
		setSize(d);
		setMinimumSize(d);
		setLocation(new Point(200, 100));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		p_content = new JPanel();
		p_content.setBorder(new EmptyBorder(5, 5, 5, 5));
		setContentPane(p_content);
		GridBagLayout gbl = new GridBagLayout();
		gbl.columnWidths = new int[]{0, 0, 0};
		gbl.rowHeights = new int[]{0, 0, 0, 0, 0, 0};
		gbl.columnWeights = new double[]{1.0, 1.0, Double.MIN_VALUE};
		gbl.rowWeights = new double[]{0.0, 1.0, 0.0, 1.0, 0.0, Double.MIN_VALUE};
		p_content.setLayout(gbl);

		Font f_mono = new Font("monospaced", Font.PLAIN, 14);
		Border text_border = BorderFactory.createEmptyBorder(4, 4, 4, 4);

		JLabel l_css = new JLabel("Contenu CSS");
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 5, 5);
			gbc.gridx = 0;
			gbc.gridy = 0;
			p_content.add(l_css, gbc);
		}

		JScrollPane sp_css = new JScrollPane();
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 0, 5);
			gbc.fill = GridBagConstraints.BOTH;
			gbc.gridx = 0;
			gbc.gridy = 1;
			p_content.add(sp_css, gbc);
		}

		ta_css = new JTextArea();
		ta_css.setFont(f_mono);
		ta_css.setBorder(text_border);
		ta_css.setText("/* Style CSS ici */");
		sp_css.setViewportView(ta_css);

		JLabel l_html = new JLabel("HTML source");
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 5, 5);
			gbc.gridx = 0;
			gbc.gridy = 2;
			p_content.add(l_html, gbc);
		}

		JScrollPane sp_html_body = new JScrollPane();
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 0, 5);
			gbc.fill = GridBagConstraints.BOTH;
			gbc.gridx = 0;
			gbc.gridy = 3;
			p_content.add(sp_html_body, gbc);
		}

		ta_html_body = new JTextArea();
		ta_html_body.setFont(f_mono);
		ta_html_body.setBorder(text_border);
		sp_html_body.setViewportView(ta_html_body);

		JButton b_disp_html = new JButton("Afficher \u2192"); // "Afficher ->"
		b_disp_html.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{ doHtmlRender(); }
		});
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 5, 5);
			gbc.gridx = 0;
			gbc.gridy = 4;
			p_content.add(b_disp_html, gbc);
		}

		JLabel l_disp_html = new JLabel("HTML affiché");
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.insets = new Insets(0, 0, 5, 0);
			gbc.gridx = 1;
			gbc.gridy = 0;
			p_content.add(l_disp_html, gbc);
		}

		JScrollPane sp_html = new JScrollPane();
		{
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.fill = GridBagConstraints.BOTH;
			gbc.gridx = 1;
			gbc.gridy = 1;
			gbc.gridheight = 3;
			p_content.add(sp_html, gbc);
		}

		tp_html = new JTextPane();
		sp_html.setViewportView(tp_html);
	}

	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				FenetreRenduHtml frame = new FenetreRenduHtml();
				frame.setVisible(true);
			}
		});
	}
}

Le contenu HTML peut faire référence à des images. Par défaut, les chemins relatifs ne sont pas supportés, seul un chemin absolu permet l'affichage de l'image. Les URLs distantes sont également supportées pour les protocoles supportés par Java (HTTP, HTTPS).

 

Les chemins et URL relatifs sont supportés quand une URL de base est fournie à la méthode setPage(url) (url de type java.lang.String ou java.net.URL) qui charge la page correspondante. Cette méthode permet le chargement de pages HTML simples (HTML 4 et CSS 1), sans Javascript. Elle ne peut donc pas servir à afficher un site web.


Créer un composant

La bibliothèque Swing propose une riche collection de composants répondant aux besoins les plus courants concernant la création d'une interface graphique. Cependant, certaines applications peuvent avoir besoin de composants spécifiques, qu'il soit basé sur un composant existant (comme une case à cocher à trois états par exemple), ou qu'il ne soit basé sur aucun composant existant. C'est par exemple le cas d'un feu tricolore utilisé dans un jeu sur la circulation routière, qui sera développé dans ce chapitre par étapes.

Création de la classe

modifier

La première version de la classe ci-dessous définit les couleurs utilisées et l'état courant du feu tricolore. La méthode statique principale crée une fenêtre pour tester le composant. Le composant n'est pas visible pour le moment avec cette première version du code.

package org.wikibooks.fr.swing.component;

import java.awt.*;

import javax.swing.*;

/**
 * Composant de feu tricolore.
 * @author fr.wikibooks.org
 */
public class FeuTricolore extends JComponent
{
	// Couleurs du feu
	private static final Color// R    V    B
		FEU_ETEINT = new Color( 32,  32,  32),
		FEU_VERT   = new Color(  0, 180,   0),
		FEU_ORANGE = new Color(255, 160,   0),
		FEU_ROUGE  = new Color(255,   0,   0);

	static enum EtatFeu
	{
		ETEINT,
		VERT,
		ORANGE,
		ROUGE
	};

	private EtatFeu etat_courant = EtatFeu.ETEINT;

	public FeuTricolore()
	{
		Dimension d = new Dimension(40,120);
		setMinimumSize(d);
		setPreferredSize(d);
		setBackground(Color.BLACK);
		setForeground(Color.DARK_GRAY);
	}

	// ...

	// Méthode main utilisée pour tester le composant dans une fenêtre
	public static void main(String[] args)
	{
		// Créer la fenêtre
		JFrame f = new JFrame("Feu tricolore");
		f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

		// Personaliser le panneau du contenu
		JPanel p_content = new JPanel();
		p_content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
		f.setContentPane(p_content);

		// Ajouter un feu tricolore
		p_content.add(new FeuTricolore());
		
		f.pack();
		f.setVisible(true);
	}
}

État du composant

modifier

L'état du composant est représenté par une énumération de type EtatFeu

static enum EtatFeu
{
	ETEINT,
	VERT,
	ORANGE,
	ROUGE
};

private EtatFeu etat_courant = EtatFeu.ETEINT;

On ajoute également des accesseurs permettant de lire et écrire l'état courant du feu :

/**
 * Obtenir l'état courant du feu.
 * @return L'état courant du feu.
 */
public EtatFeu getEtatCourant()
{
	return etat_courant;
}

/**
 * Définir l'état courant du feu.
 * @param etat_courant Le nouvel état courant du feu.
 */
public void setEtatCourant(EtatFeu etat_courant)
{
	this.etat_courant = etat_courant;
	repaint();
}

La méthode de changement d'état appelle la méthode repaint() pour mettre à jour l'affichage aussitôt.

Dessin du composant

modifier

Le dessin d'un composant Swing est effectué par la méthode publique void paint(Graphics g). Cette méthode délègue le travail à trois autres méthodes protégées appelées dans l'ordre suivant :

void paintComponent(Graphics g)
Affiche le contenu du composant lui-même.
void paintBorder(Graphics g)
Affiche les bords du composant dans la zone définie par les marges de type Insets.
void paintChildren(Graphics g)
Affiche les composants contenus dans celui-ci, par appel à leur méthode paint.

Cet ordre assure que les composants contenus soit affichés au-dessus du composant. Pour définir l'apparence du composant, une sous-classe ne doit redéfinir que la méthode void paintComponent(Graphics g). Celle-ci doit éviter de dessiner dans la zone définie par les marges de type Insets qui est réservée aux bords (Border).

 

Il existe aussi une méthode void paintComponents(Graphics g) permettant de redessiner les composants contenus, au lieu du composant. Si le composant n'affiche rien, vérifiez qu'il n'y a pas de 's' à la fin du nom de la méthode. Cette erreur peut arriver facilement avec un IDE proposant la complétion automatique.

Le code ci-dessous ajoute la méthode de dessin du feu tricolore. Elle utilise un tableau à deux dimensions définissant la couleur de chaque feu pour chaque état possible.

// Couleur des feux selon l'état courant
private Color[][] COULEUR_FEUX = // [état][feu]
{
	{ FEU_ETEINT, FEU_ETEINT, FEU_ETEINT }, // ETEINT
	{ FEU_ETEINT, FEU_ETEINT, FEU_VERT   }, // VERT
	{ FEU_ETEINT, FEU_ORANGE, FEU_ETEINT }, // ORANGE
	{ FEU_ROUGE,  FEU_ETEINT, FEU_ETEINT }, // ROUGE
};

@Override
protected void paintComponent(Graphics g)
{
	Dimension d = getSize();
	// Le fond
	g.setColor(getBackground());
	g.fillRect(0, 0, d.width, d.height);

	// Dimension d'un feu selon la taille du composant
	int fw = d.width, fh = d.height/3;
	// ...de forme carrée :
	if (fw>fh) fw=fh; else fh = fw;

	int dm = fw*3/4, // Diamètre : 75% de la taille disponible
		ecart = fw-dm, // Décalage de 25% = 100%-75%
		// Position centrée avec décalage
		x = (d.width  - fw   + ecart)/2,
		y = (d.height - 3*fh + ecart)/2;

	// Les trois feux de haut en bas
	Color[] couleurs = COULEUR_FEUX[etat_courant.ordinal()];
	for(int i=0 ; i<3 ; i++,y+=fh)
	{
		g.setColor(couleurs[i]);
		g.fillOval(x, y, dm, dm);
		g.setColor(getForeground());
		g.drawOval(x, y, dm-1, dm-1); // dm-1 pour ne pas déborder
	}
}

Gestion des évènements

modifier

Dans l'état actuel du code, le feu affiche l'état courant initial éteint. Cette section définit le code pour changer l'état courant sur réception d'un évènement de clic de souris.

On ajoute la méthode qui met l'état suivant selon l'état courant :

protected void cycleEtatCourant()
{
	switch(etat_courant)
	{
	case ETEINT: setEtatCourant(EtatFeu.ROUGE);  break;
	case VERT:   setEtatCourant(EtatFeu.ORANGE); break;
	case ORANGE: setEtatCourant(EtatFeu.ROUGE);  break;
	case ROUGE:  setEtatCourant(EtatFeu.VERT);   break;
	}
}

Cette méthode est appelée par le gestionnaire de clic de souris ajouté au composant dans le constructeur de la classe :

addMouseListener(new MouseAdapter()
{
	@Override
	public void mouseClicked(MouseEvent e)
	{
		cycleEtatCourant();
	}
});

Améliorations

modifier

La classe peut être améliorée sur divers points abordés dans cette section.

Changement d'état

modifier
 
Les 3 feux tricolores orchestrés par un gestionnaire d'état

Afin que le feu ait un comportement plus proche de la réalité, le changement d'état du feu devrait se faire sans intervention de l'utilisateur, au bout d'un certain temps dépendant de l'état courant.

Tout d'abord, afin d'éviter de réafficher le composant trop souvent, la méthode de changement d'état est modifiée comme ci-dessous :

public void setEtatCourant(EtatFeu etat_courant)
{
	// Seulement si changement d'état
	if (etat_courant != this.etat_courant)
	{
		this.etat_courant = etat_courant;
		repaint();
	}
}

On supprime donc le changement d'état par clic de souris, c'est à dire le code ajouté dans la section « Gestion des évènements » :

  • Supprimer la méthode cycleEtatCourant(),
  • Et supprimer l'appel à la méthode addMouseListener dans le constructeur.

La gestion d'un croisement implique plusieurs feux, affectés à différentes voies de circulation (à double sens ou non). Il est donc préférable que le changement d'état d'un feu soit géré par une nouvelle classe afin de synchroniser les feux du carrefour. Il faut donc déplacer l'énumération dans une classe à part :

package org.wikibooks.fr.swing.component;

public enum EtatFeu
{
	ETEINT,
	VERT,
	ORANGE,
	ROUGE
}

Ensuite, il faut créer une nouvelle classe qui sera nommée GestionnaireFeu, qui va gérer une liste de feux par voie de circulation, définir la voie de circulation active (cycle rouge-vert-orange-rouge) les autres étant inactives (feu rouge), et définir un thread mettant à jour le cycle courant :

package org.wikibooks.fr.swing.component;

import java.util.*;

/**
	Gestionnaire de l'état des feux tricolores d'un carrefour.
	<h3>Fonctionnement</h3>
	Une seule voie est active à la fois, les autres
	voies ont le feu au rouge.
	La voie active effectue un cycle où le feu prend
	successivement les couleurs suivantes :<ul>
	<li>rouge pendant marge_duree_rouge cycles élémentaires,</li>
	<li>vert pendant duree_vert cycles élémentaires,</li>
	<li>orange pendant duree_orange cycles élémentaires,</li>
	<li>rouge pendant marge_duree_rouge cycles élémentaires.</li>
	</ul>
	À la fin de ce cycle, la voie devient inactive,
	et la voie suivante devient active.
 * @author fr.wikibooks.org
 */
public class GestionnaireFeu
{
	private int duree_cycle_elementaire = 1000; // ms

	/** Couleur de feu courante pour la voie active. */
	private EtatFeu etat_voie_active = EtatFeu.ROUGE; // Valeur initiale pour la sécurité
	/** Index de la voie active. */
	private int voie_active = 0;
	/** Liste des feux pour chaque voie. */
	private ArrayList<FeuTricolore>[] feux_par_voie;

	private final int duree_vert, duree_orange, marge_duree_rouge;
	private final int total_cycle, total_period;

	private final Object lock = new Object();

	private void boucleCycleFeux()
	{
		try
		{
			long time = System.currentTimeMillis(), t;
			int index_cycle = 0;
			for(;;)
			{
				// Mise à jour de l'état
				voie_active = index_cycle / total_cycle;
				int n = index_cycle % total_cycle;
				if (n < marge_duree_rouge) etat_voie_active = EtatFeu.ROUGE;
				else
				{
					n -= marge_duree_rouge;
					if (n < duree_vert) etat_voie_active = EtatFeu.VERT;
					else
					{
						n -= duree_vert;
						if (n < duree_orange) etat_voie_active = EtatFeu.ORANGE;
						else etat_voie_active = EtatFeu.ROUGE;
					}
				}
				
				// Mise à jour des feux
				synchronized(lock)
				{
					for(int voie=0 ; voie<feux_par_voie.length; voie++)
					{
						EtatFeu etat = voie==voie_active ? etat_voie_active : EtatFeu.ROUGE;
						for(FeuTricolore feu : feux_par_voie[voie])
							feu.setEtatCourant(etat);
					}
				}

				// Cycle élémentaire suivant
				index_cycle = (index_cycle+1) % total_period;

				// Attendre jusqu'à l'heure du prochain cycle élémentaire
				time += duree_cycle_elementaire;
				t = System.currentTimeMillis();
				if (t<time) Thread.sleep(time - t);
			}
		}
		catch(InterruptedException ex)
		{ /* Thread interrompu */ }
	}

	public void add(int voie, FeuTricolore feu)
	{
		if (voie<0 || voie>=feux_par_voie.length)
			throw new IllegalArgumentException("Voie non valide : "+voie);
		if (feu == null)
			throw new NullPointerException("Pas de feu tricolore spécifié");

		synchronized(lock)
		{
			feux_par_voie[voie].add(feu);
			EtatFeu etat = voie==voie_active ? etat_voie_active : EtatFeu.ROUGE;
			feu.setEtatCourant(etat);
		}
	}

	public GestionnaireFeu()
	{
		this(2);
	}

	public GestionnaireFeu(int nombre_de_voies)
	{
		this(nombre_de_voies, 8, 2, 1);
	}

	/**
	 * Construire un gestionnaire de feux.
	 * @param nombre_de_voies Nombre de voies de circulation.
	 * @param duree_vert Durée du feu au vert en nombre de cycles élémentaires (1 seconde par défaut).
	 * @param duree_orange Durée du feu au orange en nombre de cycles élémentaires (1 seconde par défaut)
	 * @param marge_duree_rouge Marge de durée du feu au rouge en nombre de cycles élémentaires (1 seconde par défaut)
	 */
	public GestionnaireFeu(int nombre_de_voies,
		int duree_vert, int duree_orange, int marge_duree_rouge)
	{
		if (nombre_de_voies<2 ||
			duree_vert<1 ||
			duree_orange<1 ||
			marge_duree_rouge<0)
			throw new IllegalArgumentException("Un argument est invalide");

		this.duree_vert = duree_vert;
		this.duree_orange = duree_orange;
		this.marge_duree_rouge = marge_duree_rouge;
		this.total_cycle = 2*marge_duree_rouge + duree_vert + duree_orange;
		this.total_period = this.total_cycle * nombre_de_voies;

		feux_par_voie = new ArrayList[nombre_de_voies];
		for(int i=0 ; i<feux_par_voie.length ; i++)
			feux_par_voie[i] = new ArrayList<FeuTricolore>();

		// Le thread mettant à jour les feux en tâche de fond :
		Thread th = new Thread(new Runnable()
		{
			@Override
			public void run()
			{ boucleCycleFeux(); }
		}, "Gestion des feux tricolores");
		th.setDaemon(true); // Ne maintient pas l'application en vie.
		th.start();
	}
}

La méthode statique de lancement permettant de montrer trois feux sur trois voies de circulation différentes est la suivante :

public static void main(String[] args)
{
	// Créer la fenêtre
	JFrame f = new JFrame("Feu tricolore");
	f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

	// Personnaliser le panneau du contenu
	JPanel p_content = new JPanel();
	p_content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
	p_content.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 20));
	f.setContentPane(p_content);

	int nb_voies = 3;
	GestionnaireFeu gestionnaire = new GestionnaireFeu(nb_voies);
	// Ajouter un feu tricolore par voie
	for(int i=0 ; i<nb_voies ; i++)
	{
		FeuTricolore feu = new FeuTricolore();
		p_content.add(feu);
		gestionnaire.add(i, feu);
	}
	
	f.pack();
	f.setVisible(true);
}

Les cycles des feux sur les trois voies pour les temps par défaut sont résumés dans le tableau ci-dessous :

Voie 1 active Voie 2 active Voie 3 active
Voie 1
Voie 2
Voie 3

Qualité de l'affichage du composant

modifier

Le dessin circulaire du composant montre un crénelage causé par le tracé pixel par pixel. Il est possible d'améliorer l'affichage en utilisant l'antialiasing, en modifiant légèrement le code.

Tout d'abord, il faut définir les propriétés de rendu dans un dictionnaire utilisant des clés de la classe RenderingHints. La partie de code ci-dessous est à ajouter dans la classe FeuTricolore, et montre les clés modifiables. Celles sur l'affichage du texte (KEY_TEXT_ANTIALIASING, KEY_FRACTIONALMETRICS) ne sont pas nécessaires pour cette classe mais illustrent les possibilités.

protected static final HashMap<Key, Object> QUALITY_HINTS =
	new HashMap<RenderingHints.Key, Object>();
static
{
	QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//	QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
	QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
	QUALITY_HINTS.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
	QUALITY_HINTS.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
	QUALITY_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
	QUALITY_HINTS.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
	QUALITY_HINTS.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}

On peut aussi améliorer le tracé en utilisant une plus grande épaisseur de trait. Les paramètres de trait sont gérés par la classe abstraite Stroke, dont la classe BasicStroke est une implémentation.

private Stroke STYLE_TRAIT = new BasicStroke(2); // Épaisseur : 2 pixels
 
Les 3 feux tricolores avec une meilleure qualité de rendu

Il ne reste plus qu'à utiliser ces éléments dans la méthode paintComponent. Cependant ils ne sont utilisables qu'avec une instance de la classe Graphics2D, qui est la classe réelle de l'objet Graphics passé à la méthode. Il faut donc convertir l'objet pour définir les options de rendu et le style des traits.

@Override
protected void paintComponent(Graphics graphics)
{
	Graphics2D g = (Graphics2D)graphics;
	Dimension d = getSize();
	g.addRenderingHints(QUALITY_HINTS);
	g.setStroke(STYLE_TRAIT);

// ... la suite reste inchangée, identique à la version précédente.
}

Préservation du contexte graphique

modifier

La méthode paint étant implémentée par appels à d'autres méthodes, elle leur passe le même contexte graphique de classe Graphics2D. Cela signifie que le dernier changement de paramètre (couleur, police de caractère, style de trait, région clip, ...) est celui par défaut pour la méthode appelée après. Il est donc recommandé de restaurer les valeurs d'origine avant de terminer. Cependant, cela implique de lire la valeur au début de la méthode pour la restaurer à la fin, ce qui complique le codage vu le nombre possible de paramètres auquel cela s'applique.

La solution est de créer un contexte dérivé de celui passé en paramètre, et de ne plus utiliser l'original ensuite. Il faudra cependant libérer les ressources prises par le nouveau contexte en appelant sa méthode dispose() à la fin.

@Override
protected void paintComponent(Graphics graphics)
{
	Graphics2D g = (Graphics2D)graphics.create();
	try
	{
		Dimension d = getSize();
		g.addRenderingHints(QUALITY_HINTS);
		g.setStroke(STYLE_TRAIT);

	// ... la suite reste inchangée, identique à la version précédente, indentée d'un cran.
	}
	finally
	{
		g.dispose();
	}
}

Orientation du composant

modifier

Le feu tricolore utilisé dans une simulation de circulation réaliste avec vue de dessus d'un carrefour devrait pouvoir être positionné horizontalement de haut en bas et inversement, ou verticalement de droite à gauche et inversement.


Génération d'évènements spécifiques

modifier

Dans une utilisation réaliste du composant, le feu tricolore utilisé pour simuler la circulation devrait être capable de notifier ses changements d'états aux automobilistes pour qu'ils puissent s'arrêter ou redémarrer.


Créer une case à cocher à trois états

Ce chapitre complète le sujet du précédent en montrant comment créer un composant à partir d'un composant existant. Ce chapitre décrira la création d'une case à cocher à trois états :

  •   Coché : L'option est sélectionnée,
  •   Non coché : L'option n'est pas sélectionnée,
  •   Inconnu ou partiel : Il s'agit d'un état utilisé soit quand l'option n'a pas d'état défini par l'utilisateur, soit parce qu'une partie des éléments qu'implique la sélection de l'option est sélectionnée (sélection partielle).

Base du composant

modifier

La case à cocher sera basée sur un label (classe JLabel) pour afficher le texte, associé à une icône pour afficher l'état. L'état du composant dépendra de l'état de la case, mais aussi du fait que la souris soit au-dessus du composant ou non. Le tableau ci-dessous présente les différentes images utilisées, avec le nom du fichier utilisé par le code du composant pour charger l'image. Téléchargez les images sous le nom indiqué, dans le répertoire du package de la classe.

État Non coché Inconnu / partiel Coché
Normal  

checkbox_0.png

 

checkbox_1.png

 

checkbox_2.png

Survol de la souris  

checkbox_0_over.png

 

checkbox_1_over.png

 

checkbox_2_over.png

État du composant

modifier

Le composant a trois états possibles. Contrairement à une case à cocher normale, le type booléen ne suffit plus à représenter l'état. Il faut donc créer une énumération :

package org.wikibooks.fr.swing.component;

/**
 * Trois états d'une case à cocher.
 * @author fr.wikibooks.org
 */
public enum ETristate
{
	/** Non coché */
	UNCHECKED,
	/** Inconnu ou sélection partielle. */
	PARTIAL,
	/** Coché */
	CHECKED;
// ...
}

On ajoute deux méthodes retournant l'état suivant, selon deux modes de transition différents :

  • Mode trois états : Non coché → Coché → Inconnu → Non coché.
  • Mode deux états (case à cocher classique) : Inconnu → Non coché → Coché → Noncoché.
/**
 * Get next state in the following tri-state cycle: {@link #UNCHECKED} -> {@link #CHECKED} -> {@link #PARTIAL} -> {@link #UNCHECKED}.
 * @return The next state in the tri-state cycle.
 * @see #getNextDual()
 */
public ETristate getNext()
{
	switch(this)
	{
	case UNCHECKED: return CHECKED;
	case PARTIAL: return UNCHECKED;
	default: return PARTIAL;
	}
}

/**
 * Get next state in the following dual-state cycle: {@link #PARTIAL} -> {@link #UNCHECKED} -> {@link #CHECKED} -> {@link #UNCHECKED}.
 * @return The next state in the dual-state cycle.
 * @see #getNext()
 */
public ETristate getNextDual()
{
	switch(this)
	{
	case UNCHECKED: return CHECKED;
	default: return UNCHECKED;
	}
}

Classe de la case à cocher

modifier

Le code ci-dessous présente la première version de la classe du composant, complétée au fil des sections de ce chapitre. La classe implémente l'interface java.awt.ItemSelectable des objets contenant des éléments sélectionnables (la case à cocher elle-même).

package org.wikibooks.fr.swing.component;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;

/**
 * Case à cocher à trois états.
 * @author fr.wikibooks.org
 */
public class TristateCheckbox extends JLabel
implements ItemSelectable
{
	private static Icon[] icons;

	static
	{
		// Chargement des icônes représentant l'état de la case à cocher :
		icons = new Icon[6];
		for(int i=0,j=0;i<3;i++)
		{
			icons[j++] = new ImageIcon(TristateCheckbox.class.getResource("checkbox_"+i+".png"));
			icons[j++] = new ImageIcon(TristateCheckbox.class.getResource("checkbox_"+i+"_over.png"));
		}
	}

	// L'état de la case à cocher
	protected ETristate state = ETristate.UNCHECKED;

	// Indicateur de souris survolant le composant, et de mode à deux ou trois états.
	protected boolean mouse_over = false, dual_state = false;
	private int i_state = 0; // Index de l'état courant

	// Écouteur d'évènements pour la case à cocher :
	ArrayList<ItemListener> ll_item = new ArrayList<ItemListener>();

	protected void fireItemEvent(boolean selected)
	{
		ItemEvent e = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED,
			this, selected ? ItemEvent.SELECTED : ItemEvent.DESELECTED);
		for(ItemListener l : ll_item)
		{
			l.itemStateChanged(e);
		}
	}

	@Override
	public void addItemListener(ItemListener l)
	{ ll_item.add(l); }

	@Override
	public Object[] getSelectedObjects()
	{ return state==ETristate.UNCHECKED?null:new Object[]{this}; }

	@Override
	public void removeItemListener(ItemListener l)
	{ ll_item.remove(l); }

// ... à compléter
}

Gestion de l'état du composant

modifier

La méthode suivante est appelée par plusieurs autres méthodes pour mettre à jour l'icône affichée en fonction de l'état et des indicateurs de la case à cocher.

/**
 * Met à jour l'icône si nécessaire.
 */
protected void updateState()
{
	int n = i_state;
	i_state = (mouse_over?1:0) | (state.ordinal()<<1);
	if (i_state != n)
		setIcon(icons[i_state]);
	// Notifier les écouteurs enregistrés du changement d'état :
	fireItemEvent(state!=ETristate.UNCHECKED);
}

Elle est utilisée par exemple par la méthode modifiant l'indicateur de survol de la souris :

private void setMouseOver(boolean b)
{
	mouse_over = b;
	updateState();
}

Et également par les méthodes modifiant l'état de la case à cocher, en mode trois états ou en mode deux états.

/**
 * Get the current checkbox state.
 * @return The current state.
 */
public ETristate getState()
{
	return state;
}

// Définir l'état, en mode trois états
public void setState(ETristate state)
{
	this.state = state;
	this.dual_state = false;
	updateState();
}

// Définir l'état, en mode deux états
public void setDualState(ETristate state)
{
	this.state = state;
	this.dual_state = true;
	updateState();
}

// Définir l'état, en mode deux états, à moins que l'état soit inconnu/partiel
public void setMultipleState(ETristate state)
{
	this.state = state;
	this.dual_state = state != ETristate.PARTIAL;
	updateState();
}

La méthode nextState() met la case à cocher dans l'état suivant, selon le mode de cycle courant.

protected void nextState()
{
	// Tester si le composant est activé, car cette méthode est appelée au clic de souris.
	if (isEnabled())
	{
		state = dual_state ? state.getNextDual() : state.getNext();
		updateState();
	}
}

Déclencher une action

modifier

Beaucoup de composants Swing permettent de déclencher une action sur certains évènements. Les méthodes ci-dessous fournissent une implémentation pour définir un nom d'action et enregistrer des écouteurs de déclenchement d'action.

protected String action_command = "";

public void setActionCommand(String action_command)
{
	this.action_command = action_command;
}

public String getActionCommand()
{
	return action_command;
}

ArrayList<ActionListener> ll_action = new ArrayList<ActionListener>();

public void addActionListener(ActionListener l)
{
	ll_action.add(l);
}

public void removeActionListener(ActionListener l)
{
	ll_action.remove(l);
}

protected void fireAction(String command, int modifiers)
{
	ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
		command, System.currentTimeMillis(), modifiers);
	for(ActionListener l : ll_action)
		l.actionPerformed(e);
}

Constructeurs

modifier

Le constructeur de la case à cocher définit l'écouteur d'évènements de la souris qui appelle les méthodes vues précédemment.

// Constructeurs définissant des valeurs par défaut:

public TristateCheckbox()
{ this("", JLabel.LEFT); }

public TristateCheckbox(String label)
{ this(label, JLabel.LEFT); }

public TristateCheckbox(int align)
{ this("", align); }

// Contructeur principal
public TristateCheckbox(String label, int align)
{
	super(label, icons[0], align); // Non coché initialement
	setFocusable(true); // Peut obtenir le focus
	addMouseListener(new MouseListener()
	{
		public void mouseReleased(MouseEvent e)
		{
			// Vérifier que la souris survole encore le composant
			// au moment de relâcher le bouton.
			if (mouse_over) nextState();
			fireAction(action_command, e.getModifiersEx());
		}

		public void mousePressed(MouseEvent e)
		{
			setMouseOver(true);
		}

		public void mouseExited(MouseEvent e)
		{
			setMouseOver(false);
		}

		public void mouseEntered(MouseEvent e)
		{
			setMouseOver(true);
		}

		public void mouseClicked(MouseEvent e)
		{  }
	});
}

Test du composant

modifier

Le code source pour tester la case à cocher est présenté ci-dessous :

// Méthode main utilisée pour tester le composant dans une fenêtre
public static void main(String[] args)
{
	// Créer la fenêtre
	JFrame f = new JFrame("Case à cocher");
	f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

	// Personnaliser le panneau du contenu
	JPanel p_content = new JPanel();
	p_content.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
	p_content.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 20));
	f.setContentPane(p_content);

	TristateCheckbox tcb = new TristateCheckbox("Test");
	p_content.add(tcb);
	
	f.pack();
	f.setVisible(true);
}


Dessin d'un composant

La création d'un composant directement à partir de la classe javax.swing.JComponent nécessite de définir une méthode pour redessiner le composant quand il est visible. Ce chapitre décrit comment le faire en détails.

Le dessin d'un composant Swing est effectué par la méthode publique void paint(Graphics g). Cette méthode délègue le travail à trois autres méthodes protégées appelées dans l'ordre suivant :

void paintComponent(Graphics g)
Affiche le contenu du composant lui-même.
void paintBorder(Graphics g)
Affiche les bords du composant dans la zone définie par les marges de type Insets.
void paintChildren(Graphics g)
Affiche les composants contenus dans celui-ci, par appel à leur méthode paint.

Cet ordre assure que les composants contenus soit affichés au-dessus du composant. Pour définir l'apparence du composant, une sous-classe ne doit redéfinir que la méthode void paintComponent(Graphics g). Celle-ci doit éviter de dessiner dans la zone définie par les marges de type Insets qui est réservée aux bords (Border).

Contexte graphique

modifier

Un objet de classe java.awt.Graphics est passé aux méthodes d'affichage du composant. Il s'agit du contexte graphique, gérant les attributs courants de dessin (couleur, style des lignes, police de caractères, transformations...) et possédant des méthodes de tracé de formes, de texte, d'images...

La classe réelle du contexte graphique passé aux méthode d'affichage est java.awt.Graphics2D possédant des méthodes supplémentaires améliorant l'affichage en 2D. Il est donc nécessaire de convertir l'objet fourni pour pouvoir appeler ces méthodes avancées.

Attributs courants de dessin

modifier

Le contexte graphique possède des accesseurs de lecture et écriture pour les attributs de dessin à utiliser par les prochaines méthodes appelées :

  • Couleur courante pour dessiner : setColor(Color)
  • Police de caractère courante : setFont(Font)

Attributs supplémentaires ajoutés par la classe java.awt.Graphics2D :

  • Couleur courante pour effacer : setBackgroundColor(Color)
  • Composition avec les pixels existant : setComposite(Composite)
  • Style des lignes (épaisseur, continu/tirets/..., extrémités, jointure de lignes d'un polygone) : setStroke(Stroke)
  • Motif de remplissage : setPaint(Paint)
  • Déformation des coordonnées (translation, rotation...) : setTransform(AffineTransform)

Exemple :

@Override
protected void paintComponent(Graphics graphics)
{
	Graphics2D g = (Graphics2D)graphics;
	g.setColor(Color.BLUE);
// ... dessiner en bleu
	g.setColor(Color.WHITE);
// ... dessiner en blanc
	g.setStroke(new BasicStoke(1.5f));
// ... dessiner des lignes d'un pixel et demi d'épaisseur
}

Dessin de formes

modifier

Dans les exemples suivants, g représente le contexte graphique de classe java.awt.Graphics.

Méthode de tracé de contour de formes
Les méthodes dont le nom commence par draw dessine le contour de formes et utilise la couleur et le style de tracé courants du contexte graphique.
Méthode de tracé de formes remplies
Les méthodes dont le nom commence par fill dessine la surface remplie de formes et utilise la couleur et le motif courants du contexte graphique.

Les formes fermées possèdent à la fois une méthode de tracé du contour et une méthode de tracé de la forme remplie.

Tracer une ligne entre deux points
g.drawLine(x1,y1, x2,y2)
La méthode drawLine trace une ligne entre les deux points dont les coordonnées sont spécifiées en arguments, en utilisant le style courant de la ligne qui par défaut est une ligne continue d'un pixel d'épaisseur.
Tracer un rectangle
g.drawRect(x,y, width,height)
La méthode drawRect trace un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Cette méthode trace un rectangle qui s'étend sur la largeur et la hauteur spécifiées additionnées à l'épaisseur courante de la ligne (1 pixel par défaut). Les largeurs ou hauteurs négatives sont acceptées.
Remplir un rectangle
g.fillRect(x,y, width,height)
La méthode fillRect trace un rectangle rempli de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Les largeurs ou hauteurs négatives ne sont pas supportées.
Tracer un rectangle 3D
g.drawRect3D(x,y, width,height, boolean raised)
La méthode drawRect trace un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Cette méthode trace un rectangle avec un bord donnant un effet 3D d'élévation quand raised=true ou d'abaissement quand raised=false. Ce rectangle s'étend sur la largeur et la hauteur spécifiées additionnées à l'épaisseur courante de la ligne (1 pixel par défaut). Les largeurs ou hauteurs négatives sont acceptées.
Remplir un rectangle 3D
g.fillRect3D(x,y, width,height, boolean raised)
La méthode fillRect3D remplit un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Cette méthode remplit un rectangle avec un bord donnant un effet 3D d'élévation quand raised=true ou d'abaissement quand raised=false. Ce rectangle s'étend sur la largeur et la hauteur spécifiées additionnées à l'épaisseur courante de la ligne (1 pixel par défaut). Les largeurs ou hauteurs négatives ne sont pas supportées.
Tracer un rectangle arrondi
g.drawRect(x,y, width,height, arc_width, arc_height)
La méthode drawRect trace un rectangle arrondi de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Les coins du rectangle sont arrondis avec des quarts d'ellipse dont les diamètres horizontaux et verticaux sont spécifiés par les arguments arc_width et arc_height respectivement. Cette méthode trace un rectangle qui s'étend sur la largeur et la hauteur spécifiées additionnées à l'épaisseur courante de la ligne (1 pixel par défaut). Les largeurs ou hauteurs négatives sont acceptées.
Remplir un rectangle arrondi
g.fillRect(x,y, width,height, arc_width, arc_height)
La méthode fillRect trace un rectangle arrondi rempli de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Les coins du rectangle sont arrondis avec des quarts d'ellipse dont les diamètres horizontal et vertical sont spécifiés par les arguments arc_width et arc_height respectivement. Les largeurs ou hauteurs négatives ne sont pas supportées.
Tracer un ovale
g.drawOval(x,y, width,height)
La méthode drawOval trace un ovale contenu dans un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Cette méthode trace un ovale qui s'étend sur la largeur et la hauteur spécifiées additionnées à l'épaisseur courante de la ligne (1 pixel par défaut). Les largeurs ou hauteurs négatives sont acceptées.
Remplir un ovale
g.fillOval(x,y, width,height)
La méthode fillOval trace un ovale rempli contenu dans un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Les largeurs ou hauteurs négatives ne sont pas supportées.
Tracer un arc
g.drawArc(x,y, width,height, start_angle, ext_angle)
La méthode drawArc trace un arc comme une partie d'un ovale contenu dans un rectangle de la taille spécifiée à partir des coordonnées du point supérieur gauche spécifiées. Cette méthode trace l'arc en démarrant à l'angle spécifié en radians, et qui s'étend sur le nombre de radians spécifié. Les largeurs ou hauteurs négatives sont acceptées.
Tracer un polygone
g.drawPolygon(int[] x, int[] y, int point_count)
g.drawPolygon(Polygon p)
La méthode drawPolygon trace un polygone à partir des deux tableaux de coordonnées spécifiés soit directement, soit indirectement en utilisant un objet de classe java.awt.Polygon. Le dernier point est relié au premier pour fermer le polygone.
Remplir un polygone
g.fillPolygon(int[] x, int[] y, int point_count)
g.fillPolygon(Polygon p)
La méthode fillPolygon remplit un polygone à partir des deux tableaux de coordonnées spécifiés soit directement, soit indirectement en utilisant un objet de classe java.awt.Polygon. Le dernier point est relié au premier pour fermer le polygone.
Tracer une ligne multipoints
g.drawPolyline(int[] x, int[] y, int point_count)
La méthode drawPolyline trace une ligne entre les points à partir des deux tableaux de coordonnées spécifiés directement. Le dernier point n'est pas relié au premier.

Dessin de forme quelconque

modifier

L'interface java.awt.Shape définit les méthodes communes des formes pour tester l'appartenance d'un point ou d'un rectangle, tester si la forme intersecte un rectangle, obtenir le rectangle englobant, et obtenir un itérateur de chemin parcourant le contour de la forme. Ce dernier groupe de méthodes est utilisé pour le dessin de la forme.

Tracer une forme quelconque
g.draw(Shape)
La méthode draw trace le contour de la forme spécifiée.
Remplir une forme quelconque
g.fill(Shape)
La méthode fill remplit la surface de la forme spécifiée.

Différentes classes du package java.awt.geom implémentent cette interface. Celles dont le nom se termine par 2D sont des classes abstraites possédant deux sous-classes concrètes statiques internes appelées Double et Float permettant d'utiliser des coordonnées de type double et float respectivement.

  • Line2D, Line2D.Double, Line2D.Float
  • Rectangle2D, Rectangle2D.Double, Rectangle2D.Float
  • Rectangle dérive de la classe Rectangle2D pour des coordonnées de type int.
  • RoundRectangle2D, RoundRectangle2D.Double, RoundRectangle2D.Float
  • Polygon
  • Path2D, Path2D.Double, Path2D.Float
  • Arc2D, Arc2D.Double, Arc2D.Float
  • Ellipse2D, Ellipse2D.Double, Ellipse2D.Float
  • CubicCurve2D, CubicCurve2D.Double, CubicCurve2D.Float
  • QuadCurve2D, QuadCurve2D.Double, QuadCurve2D.Float
  • Area englobe une autre forme et permet des opérations ensemblistes pour composer une aire complexe : union (méthode add), intersection (méthode intersect), soustraction (méthode subtract), soustraction symétrique (méthode exclusiveOr).

Dessin de texte

modifier

Les méthodes de tracé de texte utilisent la couleur et la police de caractères courantes du contexte graphique. Les coordonnées spécifiées sont celles du point du début de ligne (à gauche pour le sens d'écriture de gauche à droite, ou à droite pour l'autre sens) situé au niveau de la ligne de base des caractères.

Tracer une chaîne de caractères
g.drawString(String, int x, int y)
g.drawString(String, float x, float y)
g.drawString(AttributedCharacterIterator, int x, int y)
g.drawString(AttributedCharacterIterator, float x, float y)
La méthode drawString dessine la chaîne de caractère qu'elle soit spécifiée sous la forme d'une chaîne java.lang.String, d'un séquence de caractères avec attributs java.text.AttributedCharacterIterator. La classe java.awt.Graphics2D surcharge les méthodes pour autoriser des coordonnées sous forme de nombres à virgule flottante.
Tracer un tableau de caractères
g.drawChars(char[], int offset, int length, int x, int y)
La méthode drawChars dessine la chaîne de caractère représenté par le tableau de caractères spécifié.
Tracer un tableau d'octets
g.drawBytes(byte[], int offset, int length, int x, int y)
La méthode drawChars dessine la chaîne de caractère représenté par le tableau de caractères spécifié.

Police de caractères

modifier

La police de caractères utilisée est celle définie par la méthode void setFont(Font f) du contexte graphique. Elle est initialisée avec celle assignée au composant.

Gras, italique, soulignement et barré

modifier

Les styles gras et italiques sont gérés par la classe java.awt.Font (constantes PLAIN, BOLD, ITALIC, BOLD_ITALIC).

Le soulignement doit être géré par le code de dessin après ou avant avoir tracé le texte. La classe java.awt.Font fournit des informations pour le soulignement (distance et épaisseur de trait) via une instance de la classe java.awt.font.LineMetrics. Le code ci-dessous affiche un texte souligné, en exploitant ces informations et en utilisant l'objet java.awt.FontMetrics pour mesurer le texte et obtenir la longueur de la ligne de soulignement du texte.

String texte_souligne = "Voici comment souligner le texte."
int tx = 10, ty = 20;

g.drawString(texte_souligne, tx, ty);

FontMetrics fm = g.getFontMetrics();
int tw = fm.stringWidth(texte_souligne);

LineMetrics lm = g.getFont().getLineMetrics(texte_souligne, g.getFontRenderContext());
float line_offset = lm.getUnderlineOffset();
float line_thickness = lm.getUnderlineThickness();

int ly = (int)(ty+line_offset + line_thickness /2); // Position
g.setStroke(new BasicStroke(line_thickness)); // Épaisseur
g.drawLine(tx, ly, tx+tw, ly);

Le style barré utilise le même principe en exploitant les informations retournées par les méthodes getStrikethroughOffset() et getStrikethroughThickness(). Dans le code précédent, il suffit de remplacer les valeurs affectées aux variables line_offset et line_thickness :

float line_offset = lm.getStrikethroughOffset();
float line_thickness = lm.getStrikethroughThickness();

Les styles souligné et barré ne sont pas gérés directement par la classe java.awt.Font car il s'agit d'éléments graphiques (lignes) séparés du texte, pouvant avoir une couleur différente du texte, ou une épaisseur différente de celle indiquée par la classe java.awt.font.LineMetrics.

Afficher une image

modifier

Le contexte graphique possède plusieurs variantes de la méthode drawImage permettant d'afficher une image (instance de java.awt.Image) :

drawImage(image, x, y, observateur)
Afficher l'image à sa taille normal aux coordonnées indiquées pour le coin supérieur gauche.
drawImage(image, x, y, width, height, observateur)
Afficher l'image à la taille spécifiée par width (largeur) et height (hauteur) aux coordonnées indiquées pour le coin supérieur gauche.
drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observateur)
Afficher une portion de l'image définie par les coordonnées source des coins de la zone rectangulaire sx1, sy1, sx2, sy2 dans la zone destination définie par les coordonnées source des coins de la zone rectangulaire dx1, dy1, dx2, dy2. Cette méthode permet donc à la fois d'afficher une partie de l'image et de la redimensionner si nécessaire.

Avec ces méthodes, les pixels transparents laissent voir ce qui était dessiné avant.

Paramètres communs :

image
Image à afficher (instance de java.awt.Image).
observateur
Implémentation de l'interface java.awt.image.ImageObserver à notifier de la progression du chargement de l'image. Si l'image est déjà chargée auparavant, ce paramètre peut être null.

Exemple :

// Image chargée auparavant, en dehors de toute méthode paint, par exemple :
URL url_fond = new URL("https://upload.wikimedia.org/wikipedia/commons/a/a6/VST_image_of_the_Hercules_galaxy_cluster.jpg");
Image image_fond = ImageIO.read(url_fond);

// ...
g.drawImage(image_fond, 10, 10, null);

Ces méthodes possèdent une variante acceptant une couleur à afficher comme couleur de fond pour les images transparentes.

Transformations

modifier

Composant transparent

modifier

Par défaut, un composant est opaque, ce qui signifie qu'il doit dessiner toute la zone rectangulaire correspondant à sa taille.

Si la méthode paint ou la méthode paintComponent ne dessine pas toute la zone, pour laisser paraître le fond du contenant (un composant non rectangulaire, comme un bouton arrondi par exemple), des artifacts peuvent apparaître de manière sporadique ou non car Swing réutilise le même contexte graphique pour peindre plusieurs composants. Dans ce cas, il ne faut pas oublier de déclarer le composant comme transparent pour indiquer qu'il faut un contexte graphique propre pour peindre le composant, dans le constructeur de la classe du composant :

setOpaque(false);

Qualité de l'affichage

modifier

Par défaut, les algorithmes de dessin des méthodes du contexte graphique privilégient la vitesse de rendu à la qualité. La méthode addRenderingHints(Map<?, ?> hints) de la classe java.awt.Graphics2D permet de changer différentes options pour améliorer la qualité.

Le paramètre hints est un dictionnaire associant des clés définies par les constantes KEY_ de la classe java.awt.RenderingHints à des valeurs définies par les constantes VALUE_.

Quand les paramètres de qualité ne changent pas, il est recommandé de créer le dictionnaire une seule fois, comme dans l'exemple suivant :

protected static final HashMap<Key, Object> QUALITY_HINTS =
	new HashMap<RenderingHints.Key, Object>();
static
{
	QUALITY_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
	QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
	QUALITY_HINTS.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
	QUALITY_HINTS.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
	QUALITY_HINTS.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
	QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
	QUALITY_HINTS.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
}

Ce dictionnaire est ensuite utilisable dans la méthode d'affichage du composant :

@Override
protected void paintComponent(Graphics graphics)
{
	Graphics2D g = (Graphics2D)graphics;
	g.addRenderingHints(QUALITY_HINTS);

// ... dessin du composant avec qualité améliorée
}

Propriétés générales de qualité

modifier

Les propriétés générales de qualités s'appliquent quand aucune autre propriété équivalente plus spécifique n'est applicable.

  • RenderingHints.KEY_RENDERING : Qualité générale d'affichage.
Valeur Description
RenderingHints.VALUE_RENDER_DEFAULT Option par défaut.
RenderingHints.VALUE_RENDER_SPEED Privilégier la vitesse d'affichage.
RenderingHints.VALUE_RENDER_QUALITY Privilégier la qualité d'affichage.
  •  
    Dithering alternant des pixels noirs et blancs pour afficher les échelles de gris de l'image.
    RenderingHints.KEY_DITHERING : Gestion des couleurs sur un écran où leur nombre est limité.
Valeur Description
RenderingHints.VALUE_DITHER_DEFAULT Option par défaut.
RenderingHints.VALUE_DITHER_DISABLE Pas de dithering, les couleurs sont arrondies à la plus proche sans propagation d'erreur.
RenderingHints.VALUE_DITHER_ENABLE Activer le dithering permettant la propagation d'erreur d'arrondi des couleurs sur les pixels voisins.
  • RenderingHints.KEY_ALPHA_INTERPOLATION : Qualité de la transparence des couleurs (canal alpha)
Valeur Description
RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT Option par défaut.
RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED Privilégier la vitesse de calcul de la transparence.
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY Privilégier la qualité de calcul de la transparence.

Qualité des lignes

modifier

Une partie des propriétés de qualité concerne le tracé des lignes par les méthodes dessinant un contour : drawLine, drawRect, drawOval, drawShape... Cela concerne les méthodes dont le nom commence par draw sauf drawImage, drawRenderedImage, drawRenderableImage et drawString, drawBytes, drawChars.

  • RenderingHints.KEY_ANTIALIASING : option de réduction des effets pixelisés des lignes et des bords des formes géométriques.
Valeur Description
RenderingHints.VALUE_ANTIALIAS_DEFAULT Utiliser l'option par défaut
RenderingHints.VALUE_ANTIALIAS_OFF Sans antialiasing
RenderingHints.VALUE_ANTIALIAS_ON Avec antialiasing
  • RenderingHints.KEY_STROKE_CONTROL : Autoriser une correction de la géométrie pour améliorer la qualité du tracé.
Valeur Description
RenderingHints.VALUE_STROKE_DEFAULT Utiliser l'option par défaut
RenderingHints.VALUE_STROKE_NORMALIZE Normaliser pour améliorer l'uniformité ou l'espacement des lignes et l'esthétique globale.
RenderingHints.VALUE_STROKE_PURE Géométrie non modifiée, affiché tel quel avec une précision en dessous du pixel.

Aperçu pour une épaisseur de 1.0

modifier
RenderingHints.VALUE_ANTIALIAS_OFF RenderingHints.VALUE_ANTIALIAS_ON
RenderingHints.

VALUE_STROKE_NORMALIZE

   
RenderingHints.

VALUE_STROKE_PURE

   

Aperçu pour une épaisseur de 1.5

modifier
RenderingHints.VALUE_ANTIALIAS_OFF RenderingHints.VALUE_ANTIALIAS_ON
RenderingHints.

VALUE_STROKE_NORMALIZE

   
RenderingHints.

VALUE_STROKE_PURE

   

Qualité du texte

modifier

Trois autres propriétés de qualité concerne le tracé de texte, qu'il soit sous forme de chaîne de caractère (drawString), d'un tableau d'octets (drawBytes) ou d'un tableau de caractères (drawChars).

  • RenderingHints.KEY_FRACTIONALMETRICS : utiliser une précision en dessous du pixel pour positionner les caractères selon le vecteur d'avance mis à l'échelle de la taille de la police de caractères ou bien arrondir à l'entier le plus proche.
Valeur Description
RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT Utiliser l'option par défaut
RenderingHints.VALUE_FRACTIONALMETRICS_OFF Utiliser des mesures fractionnaires.
RenderingHints.VALUE_FRACTIONALMETRICS_ON Arrondir à l'entier le plus proche.
  • RenderingHints.KEY_TEXT_ANTIALIASING : utiliser de l'antialiasing pour le texte ou bien arrondir au pixel le plus proche.
Valeur Description
RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT Utiliser l'option par défaut
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF Utiliser l'antialias.
RenderingHints.VALUE_TEXT_ANTIALIAS_ON Sans antialias.
RenderingHints.VALUE_TEXT_ANTIALIAS_GASP Utiliser l'antialias (VALUE_TEXT_ANTIALIAS_ON) ou non (VALUE_TEXT_ANTIALIAS_OFF) selon l'information de la table GASP[1] présente dans la police de caractères pour la taille utilisée. Cette information est en général présente dans les polices TrueType, cependant, si l'information n'est pas présente, la valeur par défaut de l'algorithme est utilisée (VALUE_TEXT_ANTIALIAS_DEFAULT).

Cette option peut ne pas avoir un comportement consistant quand une police de caractères logique est utilisée vu qu'elle est composée de plusieurs polices physiques.

RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB Utiliser l'antialias sur écran LCD, dans le sens horizontal, avec l'ordre de couleur des sous-pixels RGB (rouge, vert, bleu).
RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR Utiliser l'antialias sur écran LCD, dans le sens horizontal, avec l'ordre de couleur des sous-pixels BGR (bleu, vert, rouge).
RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB Utiliser l'antialias sur écran LCD, dans le sens vertical, avec l'ordre de couleur des sous-pixels RGB (rouge, vert, bleu).
RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR Utiliser l'antialias sur écran LCD, dans le sens vertical, avec l'ordre de couleur des sous-pixels BGR (bleu, vert, rouge).
  • RenderingHints.KEY_TEXT_LCD_CONTRAST : ajustement du contraste pour l'antialiasing sur écran LCD.
Valeur Description
int Valeur entre 100 et 250 pour ajuster le contraste du texte. Typiquement en noir sur fond blanc, la valeur 100 donne un contraste fort, et 250 un contraste faible. L'intervalle typique est entre 140 et 180.

Le réglage de l'antialiasing du texte dont le nom de la valeur contient LCD est réservé aux écrans LCD ou assimilés où les pixels voisins sont tellement proches qu'il est possible que la composante bleue du pixel se mélange à la rouge du pixel voisin. Ce mélange permettant d'augmenter artificiellement la densité de pixels est la technique utilisée pour l'antialiasing LCD du texte, avec différentes variantes selon la disposition des composantes de couleur composant les pixels de l'écran. La valeur à utiliser dépend donc du modèle de l'écran.

 

Dans l'image ci-dessus, l'antialiasing de texte est RenderingHints.VALUE_TEXT_ANTIALIAS_HRGB : H pour Horizontal, RGB représentant l'ordre des composants des pixels de gauche à droite : rouge, vert, bleu. La couleur rouge borde le côté gauche des lettres en noir pour se mélanger au fond blanc, tandis que du bleu borde le côté opposé des lettres. Les côtés des couleurs rouges et bleues serait inversés pour un texte blanc sur fond noir.

Antialiasing LCD ordre des composants rouge, vert, bleu dans le sens horizontal (RenderingHints.VALUE_TEXT_ANTIALIAS_HRGB)
Zoom sur le bord gauche des lettres en noir sur fond blanc Zoom sur le bord droit des lettres en noir sur fond blanc
blanc rouge noir noir bleu blanc

Sur un écran LCD dont l'ordre des composants correspond, la composante bleue se mélange au rouge et vert à proximité, idem pour le rouge à droite avec les composantes bleue et verte.

Sur un écran LCD dont l'ordre des composants serait l'ordre inverse cela donnerait un effet indésirable de tracé parallèle rendant le texte flou :

Antialiasing LCD ordre des composants rouge, vert, bleu dans le sens horizontal (RenderingHints.VALUE_TEXT_ANTIALIAS_HRGB) sur un écran dont l'ordre des composants est bleu, vert, rouge
Zoom sur le bord gauche des lettres en noir sur fond blanc Zoom sur le bord droit des lettres en noir sur fond blanc
blanc rouge noir noir bleu blanc

Il faut dans ce cas utiliser l'ordre inverse :

Antialiasing LCD ordre des composants bleu, vert, rouge dans le sens horizontal (RenderingHints.VALUE_TEXT_ANTIALIAS_HBGR) sur un écran dont l'ordre des composants est bleu, vert, rouge
Zoom sur le bord gauche des lettres en noir sur fond blanc Zoom sur le bord droit des lettres en noir sur fond blanc
blanc bleu noir noir rouge blanc

Sur un écran non LCD, où les pixels seraient trop éloignés pour que la composante d'un pixel puissent se mélanger au pixel voisin, ou dont les pixels n'ont pas une haute densité, l'effet ne fonctionne pas correctement et un bord rouge ou bleu est visible. Pour ce cas, il est recommandé d'utiliser RenderingHints.VALUE_TEXT_ANTIALIAS_ON.

Aperçu de l'antialiasing et des mesures fractionnaires

modifier

Un aperçu de l'effet des différentes valeurs (lignes) pour RenderingHints.KEY_TEXT_ANTIALIASING (sans le préfixe commun RenderingHints.VALUE_TEXT_ANTIALIAS_, et pour le contraste par défaut) est présenté ci-dessous, combiné aux effets de RenderingHints.KEY_FRACTIONALMETRICS (colonnes).

RenderingHints.VALUE_FRACTIONALMETRICS_OFF RenderingHints.VALUE_FRACTIONALMETRICS_ON
OFF    
ON    
LCD_HRGB    
LCD_HBGR    
LCD_VRGB    
LCD_VBGR    

Aperçu de l'ajustement du contraste

modifier

Différentes valeurs d'ajustement du contraste pour VALUE_TEXT_ANTIALIAS_LCD_HRGB

100  
140  
180  
250  

Qualité des images

modifier

Les autres propriétés de qualité concerne l'affichage des images par les méthodes drawImage, drawRenderedImage et drawRenderableImage.

  • RenderingHints.KEY_INTERPOLATION : option d'affichage d'images redimensionnées.
Valeur Description
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR Pixel voisin le plus proche.
RenderingHints.VALUE_INTERPOLATION_BILINEAR La couleur d'un pixel est déterminée linéairement par les 4 pixels voisins les plus proches.
RenderingHints.VALUE_INTERPOLATION_BICUBIC La couleur d'un pixel est déterminée par une fonction cubique sur les 9 pixels voisins les plus proches.
  • RenderingHints.KEY_COLOR_RENDERING : option de qualité d'affichage des couleurs.
Valeur Description
RenderingHints.VALUE_COLOR_RENDER_DEFAULT Option par défaut.
RenderingHints.VALUE_COLOR_RENDER_SPEED Conversion rapide des couleurs.
RenderingHints.VALUE_COLOR_RENDER_QUALITY Conversion des couleurs privilégiant la qualité.

Références

modifier
  1. Grid-fitting and Scan-conversion Procedure Table https://docs.microsoft.com/en-us/typography/opentype/spec/gasp


Dessin d'une étoile

Le dessin d'un composant utilise différente formes de base : lignes, rectangles, polygones. Le but de cette section est de dessiner des étoiles de cette forme :

 

Afin d'avoir un composant générique permettant d'afficher le texte sous une série d'étoiles correspondant à un niveau d'appréciation, un niveau de qualité, ...

 
Appréciation représentée par le dessin d'étoiles.

Composant

modifier

La classe DisplayLevel ci-dessous définit le composant : état courant et couleurs utilisées. Elle sera complétée progressivement au cours de ce chapitre.

package org.wikibooks.fr.components;

import java.awt.*;

import javax.swing.*;

/**
 * Afficher un niveau avec une série d'étoiles.
 * @author fr.wikibooks.org
 */
public class DisplayLevel extends JComponent
{
	// Couleurs prédéfinies
	protected static final Color
		C_LINE = Color.BLACK,
		C_LINE_OFF = Color.LIGHT_GRAY,
		C_TEXT = Color.BLACK,
		C_FILL = new Color(240,176,20);

	protected int
		maxlevel, // Le nombre total d'étoiles.
		level;    // Le nombre d'étoiles pleines.
	protected String
		level_name; // Texte de niveau d'appréciation/qualité/...

	// Couleurs utilisées
	protected Color
		c_line = C_LINE,
		c_line_off = C_LINE_OFF,
		c_text = C_TEXT;

	public DisplayLevel()
	{
		setForeground(C_FILL);
		setMinimumSize(new Dimension(400,50));
		setPreferredSize(new Dimension(500,50));
		setSize(new Dimension(500,50));
	}

	/**
	 * Modifier le niveau affiché.
	 * @param level Niveau affiché.
	 * @param max Niveau max.
	 * @param name Texte affiché.
	 */
	public void setLevel(int level, int max, String name)
	{
		this.maxlevel = max;
		this.level = level;
		this.level_name = name;
		repaint();
	}

	// ...
}

Dessin d'une étoile

modifier
 

Les lignes de la forme n'étant pas courbées, le dessin d'un polygone sera utilisé.

Le polygone sera composé de 10 points :

  • 5 points pour les pointes, à la distance r du centre du cercle englobant (voir image ci-contre).
  • 5 points entre les pointes, à la distance s du centre du cercle englobant (voir image ci-contre).

La distance r représente le rayon du cercle englobant qui déterminera la taille des étoiles dessinées. La distance s est calculable à partir du rayon r par la formule suivante :

 

Avec  

Les points étant radialement espacés régulièrement, l'angle entre deux points est de   degrés.

Coordonnées du polygone

modifier

Les coordonnées des points du polygone sont pré-calculées et stockées dans un tableau pour un rayon de 10000 pixels. Ces coordonnées de base servent au calcul des coordonnées selon la taille d'étoile voulue, et sont relatives au centre du cercle situé aux coordonnées (0,0).

	private static final int[]
		SHAPE_X={   9511,   2245,      0,  -2245,  -9511,  -3633,  -5878,      0,   5878,   3633  },
		SHAPE_Y={  -3090,  -3090, -10000,  -3090,  -3090,   1180,   8090,   3820,   8090,   1180  };

Un objet de la classe java.awt.Polygon est utilisé pour représenter le polygone adapté à la taille de l'étoile selon celle du composant. Les points sont calculés en multipliant les coordonnées pré-calculées des 10 points par le rayon voulu et en divisant le résultat par 10000. Pour les points intermédiaires, on peut aussi ajouter un coefficient pour changer la forme de l'étoile :

	private Polygon shape; // Polygone calculé pour une certaine taille, null signifiant à recalculer.

	private static final int SHAPE_NORMAL = 100; // Coefficient nominal pour la forme de l'étoile.

	private int
		shape_size = -1,           // La taille courante pour le polygone shape.
		shape_x,                   // Abscisse du point de référence.
		shape_y,                   // Ordonnée du point de référence.
		star_shape = SHAPE_NORMAL; // Facteur de forme de l'étoile.

	/**
	 * Modifier la forme de l'étoile dessinée.
	 * @param factor_percent Facteur de forme en pourcentage :<ul>
	 * <li>inférieur à 100 pour une étoile plus fine,</li>
	 * <li>supérieur à 100 pour une étoile plus grosse.</li>
	 * </ul>
	 */
	public void setStarShape(int factor_percent)
	{
		if (star_shape != factor_percent)
		{
			star_shape = factor_percent;
			shape = null;    // Polygone à recalculer
			shape_size = -1; // Polygone à recalculer
			repaint();
		}
	}

Calcul du polygone

modifier

Le polygone est calculé, si besoin, au moment de dessiner le composant :

	/**
	 * Obtenir le polygone à utiliser pour la taille voulue.
	 * Si besoin, cette méthode recalcule le polygone.
	 * @param size Taille d'étoile.
	 * @return Le polygone à utiliser.
	 */
	private Polygon getShape(int size)
	{
		if (shape_size<0 || size!=shape_size)
		{
			// Polygone à recalculer car la taille est différente :
			shape_size = size;
			// Les 11 points du poloygone fermé, le dernier étant identique au premier.
			int[] xp=new int[11], yp=new int[11];
			final int
				r = size/2,                  // Rayon
				shpr = r*star_shape,         // Coefficient pour les points intermédiaires.
				shpmax = 10000*SHAPE_NORMAL; // Diviseur pour les points intermédiaires.
			for(int i=0 ; i<10 ; i++)
			{
				if ((i&1)==0)
				{
					xp[i] = SHAPE_X[i]*r/10000;
					yp[i] = SHAPE_Y[i]*r/10000;
				}
				else
				{
					xp[i] = SHAPE_X[i]*shpr/shpmax;
					yp[i] = SHAPE_Y[i]*shpr/shpmax;
				}
			}
			xp[10] = xp[0];
			yp[10] = yp[0];
			shape_x = -r;
			shape_y = -r;
			shape = null;
			shape = new Polygon(xp, yp, 11); // Polygone à onze points.
		}
		return shape;
	}

Les coordonnées dans le polygone shape sont fixes, mais le composant a besoin de le dessiner plusieurs fois à différents endroits. La méthode moveShapeTo ci-dessous déplace les points en mettant à jour le point de référence servant à calculer le vecteur de translation à appliquer :

	/**
	 * Déplacer le polygone au point spécifié par translation géométrique.
	 * @param x Abscisse du nouveau point de référence.
	 * @param y Ordonnée du nouveau point de référence.
	 */
	private void moveShapeTo(int x, int y)
	{
		if (x!=shape_x || y!=shape_y)
		{
			shape.translate(x-shape_x, y-shape_y);
			shape_x = x;
			shape_y = y;
		}
	}

Dessiner le composant

modifier

Il ne reste plus que le dessin du composant, utilisant plusieurs fois le polygone pour afficher les étoiles et affiche également le texte.

	@Override
	protected void paintComponent(Graphics gg)
	{
		Dimension d = getSize();
		Graphics2D g = (Graphics2D) gg;
		if (isOpaque())
		{
			g.setColor(getBackground());
			g.fillRect(0, 0, d.width, d.height);
		}
		applyHintsTo(g);

		final String s = level_name;
		g.setFont(getFont());
		FontMetrics fm = g.getFontMetrics();
		int y = 2, fha = fm.getMaxAscent(), fhb = fm.getMaxDescent(),
			hb = d.height-8-(s==null?0:fha+fhb),
			x = (d.width+4-(hb+4)*maxlevel)/2;
		if (hb>4 && maxlevel>0)
		{
			Polygon p = getShape(hb);
			for(int l=0 ; l<maxlevel ; l++)
			{
				moveShapeTo(x, y);
				if (l<level)
				{
					g.setColor(getForeground());
					g.fillPolygon(p);
					g.setColor(c_line);
				}
				else g.setColor(c_line_off);
				g.drawPolygon(p);
				x += hb+4;
			}
		}
		if (s!=null)
		{
			y += hb+4+fha;
			g.setColor(c_text);
			int tw = fm.stringWidth(s);
			x = (d.width-tw)/2;
			g.drawString(s, x, y);
		}
		super.paintComponent(gg);
	}

La méthode applyHintsTo ajuste la configuration pour améliorer la qualité d'affichage :

	protected static void applyHintsTo(Graphics2D g)
	{
		g.addRenderingHints(hm_hints);
	}

	private static HashMap<Key, Object> hm_hints = new HashMap<Key, Object>();
	static
	{
		hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
		hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
	}

Code source complet et utilisation

modifier

Le code source complet de la classe DisplayLevel est ci-dessous.

package org.wikibooks.fr.components;

import java.awt.*;
import java.awt.RenderingHints.Key;
import java.util.*;

import javax.swing.*;

/**
 * Afficher un niveau avec une série d'étoiles.
 * @author fr.wikibooks.org
 */
public class DisplayLevel extends JComponent
{
	// Couleurs prédéfinies
	protected static final Color
		C_LINE = Color.BLACK,
		C_LINE_OFF = Color.LIGHT_GRAY,
		C_TEXT = Color.BLACK,
		C_FILL = new Color(240,176,20);

	protected int
		maxlevel, // Le nombre total d'étoiles.
		level;    // Le nombre d'étoiles pleines.
	protected String
		level_name; // Texte de niveau d'appréciation/qualité/...

	// Couleurs utilisées
	protected Color
		c_line = C_LINE,
		c_line_off = C_LINE_OFF,
		c_text = C_TEXT;

	public DisplayLevel()
	{
		setForeground(C_FILL);
		setMinimumSize(new Dimension(400,50));
		setPreferredSize(new Dimension(500,50));
		setSize(new Dimension(500,50));
	}

	/**
	 * Modifier le niveau affiché.
	 * @param level Niveau affiché.
	 * @param max Niveau max.
	 * @param name Texte affiché.
	 */
	public void setLevel(int level, int max, String name)
	{
		this.maxlevel = max;
		this.level = level;
		this.level_name = name;
		repaint();
	}

	private static final int[]
		SHAPE_X={   9511,   2245,      0,  -2245,  -9511,  -3633,  -5878,      0,   5878,   3633  },
		SHAPE_Y={  -3090,  -3090, -10000,  -3090,  -3090,   1180,   8090,   3820,   8090,   1180  };

	private Polygon shape; // Polygone calculé pour une certaine taille, null signifiant à recalculer.

	private static final int SHAPE_NORMAL = 100; // Coefficient nominal pour la forme de l'étoile.

	private int
		shape_size = -1,           // La taille courante pour le polygone shape.
		shape_x,                   // Abscisse du point de référence.
		shape_y,                   // Ordonnée du point de référence.
		star_shape = SHAPE_NORMAL; // Facteur de forme de l'étoile.

	/**
	 * Modifier la forme de l'étoile dessinée.
	 * @param factor_percent Facteur de forme en pourcentage :<ul>
	 * <li>inférieur à 100 pour une étoile plus fine,</li>
	 * <li>supérieur à 100 pour une étoile plus grosse.</li>
	 * </ul>
	 */
	public void setStarShape(int factor_percent)
	{
		if (star_shape != factor_percent)
		{
			star_shape = factor_percent;
			shape = null;    // Polygone à recalculer
			shape_size = -1; // Polygone à recalculer
			repaint();
		}
	}

	/**
	 * Obtenir le polygone à utiliser pour la taille voulue.
	 * Si besoin, cette méthode recalcule le polygone.
	 * @param size Taille d'étoile.
	 * @return Le polygone à utiliser.
	 */
	private Polygon getShape(int size)
	{
		if (shape_size<0 || size!=shape_size)
		{
			// Polygone à recalculer car la taille est différente :
			shape_size = size;
			// Les 11 points du poloygone fermé, le dernier étant identique au premier.
			int[] xp=new int[11], yp=new int[11];
			final int
				r = size/2,                  // Rayon
				shpr = r*star_shape,         // Coefficient pour les points intermédiaires.
				shpmax = 10000*SHAPE_NORMAL; // Diviseur pour les points intermédiaires.
			for(int i=0 ; i<10 ; i++)
			{
				if ((i&1)==0)
				{
					xp[i] = SHAPE_X[i]*r/10000;
					yp[i] = SHAPE_Y[i]*r/10000;
				}
				else
				{
					xp[i] = SHAPE_X[i]*shpr/shpmax;
					yp[i] = SHAPE_Y[i]*shpr/shpmax;
				}
			}
			xp[10] = xp[0];
			yp[10] = yp[0];
			shape_x = -r;
			shape_y = -r;
			shape = null;
			shape = new Polygon(xp, yp, 11); // Polygone à onze points.
		}
		return shape;
	}

	/**
	 * Déplacer le polygone au point spécifié par translation géométrique.
	 * @param x Abscisse du nouveau point de référence.
	 * @param y Ordonnée du nouveau point de référence.
	 */
	private void moveShapeTo(int x, int y)
	{
		if (x!=shape_x || y!=shape_y)
		{
			shape.translate(x-shape_x, y-shape_y);
			shape_x = x;
			shape_y = y;
		}
	}

	@Override
	protected void paintComponent(Graphics gg)
	{
		Dimension d = getSize();
		Graphics2D g = (Graphics2D) gg;
		if (isOpaque())
		{
			g.setColor(getBackground());
			g.fillRect(0, 0, d.width, d.height);
		}
		applyHintsTo(g);

		final String s = level_name;
		g.setFont(getFont());
		FontMetrics fm = g.getFontMetrics();
		int y = 2, fha = fm.getMaxAscent(), fhb = fm.getMaxDescent(),
			hb = d.height-8-(s==null?0:fha+fhb),
			x = (d.width+4-(hb+4)*maxlevel)/2;
		if (hb>4 && maxlevel>0)
		{
			Polygon p = getShape(hb);
			for(int l=0 ; l<maxlevel ; l++)
			{
				moveShapeTo(x, y);
				if (l<level)
				{
					g.setColor(getForeground());
					g.fillPolygon(p);
					g.setColor(c_line);
				}
				else g.setColor(c_line_off);
				g.drawPolygon(p);
				x += hb+4;
			}
		}
		if (s!=null)
		{
			y += hb+4+fha;
			g.setColor(c_text);
			int tw = fm.stringWidth(s);
			x = (d.width-tw)/2;
			g.drawString(s, x, y);
		}
		super.paintComponent(gg);
	}

	protected static void applyHintsTo(Graphics2D g)
	{
		g.addRenderingHints(hm_hints);
	}

	private static HashMap<Key, Object> hm_hints = new HashMap<Key, Object>();
	static
	{
		hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
		hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
	}
}

Le programme ci-dessous illustre comment le composant peut être utilisé.

package org.wikibooks.fr.test;

import org.wikibooks.fr.components.*;

import java.awt.*;

import javax.swing.*;

/**
 *
 * @author fr.wikibooks.org
 */
public class DisplayTest
{
	public static void main(String[] args)
	{
		JFrame f = new JFrame("Test");
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setSize(500, 140);
		Container c = f.getContentPane();
		DisplayLevel niveau = new DisplayLevel();
		niveau.setSize(400, 100);
		c.add(niveau);
		niveau.setStarShape(120);
		niveau.setOpaque(true);
		niveau.setBackground(Color.WHITE);
		niveau.setFont(new Font("Dialog", Font.BOLD, 16));
		niveau.setLevel(3, 5, "Ce chapitre est bien"); // 3 étoiles sur 5.
		f.setVisible(true);
	}
}


Dessin de texte et effets

Le dessin d'un composant est réalisé par les méthodes paint de la classe du composant (en Swing, la méthode void paintComponent(Graphics g)) utilisant un contexte graphique.

Dessin de texte

modifier

Les méthodes de tracé de texte utilisent la couleur et la police de caractères courantes du contexte graphique. Les coordonnées spécifiées sont celles du point du début de ligne (à gauche pour le sens d'écriture de gauche à droite, ou à droite pour l'autre sens) situé au niveau de la ligne de base des caractères.

Tracer une chaîne de caractères
g.drawString(String, int x, int y)
g.drawString(String, float x, float y)
g.drawString(AttributedCharacterIterator, int x, int y)
g.drawString(AttributedCharacterIterator, float x, float y)
La méthode drawString dessine la chaîne de caractère qu'elle soit spécifiée sous la forme d'une chaîne java.lang.String, d'un séquence de caractères avec attributs java.text.AttributedCharacterIterator. La classe java.awt.Graphics2D surcharge les méthodes pour autoriser des coordonnées sous forme de nombres à virgule flottante.
Tracer un tableau de caractères
g.drawChars(char[], int offset, int length, int x, int y)
La méthode drawChars dessine la chaîne de caractère représenté par le tableau de caractères spécifié.
Tracer un tableau d'octets
g.drawBytes(byte[], int offset, int length, int x, int y)
La méthode drawChars dessine la chaîne de caractère représenté par le tableau de caractères spécifié.

Police de caractères

modifier

La police de caractères utilisée est celle définie par la méthode void setFont(Font f) du contexte graphique. Elle est initialisée avec celle assignée au composant.

Gras, italique, soulignement et barré

modifier

Les styles gras et italiques sont gérés par la classe java.awt.Font (constantes PLAIN, BOLD, ITALIC, BOLD_ITALIC).

Le soulignement doit être géré par le code de dessin après ou avant avoir tracé le texte. La classe java.awt.Font fournit des informations pour le soulignement (distance et épaisseur de trait) via une instance de la classe java.awt.font.LineMetrics. Le code ci-dessous affiche un texte souligné, en exploitant ces informations et en utilisant l'objet java.awt.FontMetrics pour mesurer le texte et obtenir la longueur de la ligne de soulignement du texte.

String texte_souligne = "Voici comment souligner le texte."
int tx = 10, ty = 20;

g.drawString(texte_souligne, tx, ty);

FontMetrics fm = g.getFontMetrics();
int tw = fm.stringWidth(texte_souligne);

LineMetrics lm = g.getFont().getLineMetrics(texte_souligne, g.getFontRenderContext());
float line_offset = lm.getUnderlineOffset();
float line_thickness = lm.getUnderlineThickness();

int ly = (int)(ty+line_offset + line_thickness /2); // Position
g.setStroke(new BasicStroke(line_thickness)); // Épaisseur
g.drawLine(tx, ly, tx+tw, ly);

Le style barré utilise le même principe en exploitant les informations retournées par les méthodes getStrikethroughOffset() et getStrikethroughThickness(). Dans le code précédent, il suffit de remplacer les valeurs affectées aux variables line_offset et line_thickness :

float line_offset = lm.getStrikethroughOffset();
float line_thickness = lm.getStrikethroughThickness();

Les styles souligné et barré ne sont pas gérés directement par la classe java.awt.Font car il s'agit d'éléments graphiques (lignes) séparés du texte, pouvant avoir une couleur différente du texte, ou une épaisseur différente de celle indiquée par la classe java.awt.font.LineMetrics.

Exemple : Texte en dégradé de couleurs

modifier

La méthode de remplissage est représentée par une instance de la classe java.awt.Paint. L'instance par défaut remplit les formes avec une couleur de manière unie. Il existe d'autres classes de remplissage dont celles permettant de peindre une forme en dégradé de couleurs.

Le tracé de texte est en fait un remplissage et il est dont possible d'utiliser un dégradé de couleurs.

 
Texte affiché en dégradé de couleurs.

Le code ci-dessous affiche une ligne de texte en dégradé de couleurs (voir image ci-dessus).

package org.wikibooks.fr.test;

import java.awt.*;
import java.util.*;

import javax.swing.*;

/**
 * Test affichage en dégradé d'un texte.
 * @author fr.wikibooks.org
 */
public class TestPaint extends JComponent
{
	public static void main(String[] args)
	{
		JFrame f = new JFrame("Test de rendu avec Java Swing");
		f.setSize(800, 300);
		f.add(new TestPaint());
		f.setVisible(true);
	}

	/** Police de caractères utilisée. */
	private static final Font F_TEXTE = new Font("Dialog", Font.BOLD, 80);

	/** Couleurs utilisées. */
	private static final Color
		C_BG = Color.BLACK,
// Orange -> Rouge
		C_FG_TEXT_1 = new Color(220,190,0),
		C_FG_TEXT_2 = new Color(250,50,0);
// Bleu -> Vert
//		C_FG_TEXT_1 = new Color(40,120,240),
//		C_FG_TEXT_2 = new Color(80,210,170);

	/** Options pour améliorer l'affichage. */
	private HashMap<RenderingHints.Key, Object> hm_hints = new HashMap<RenderingHints.Key, Object>();

	private TestPaint()
	{
		hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
		hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
		setBackground(C_BG);
	}

	@Override
	protected void paintComponent(Graphics gg)
	{
		Graphics2D g = (Graphics2D) gg;
		g.setRenderingHints(hm_hints);

		// Selon les dimensions du composant
		Dimension d = getSize();
		g.setColor(getBackground());
		g.fillRect(0, 0, d.width, d.height);

		g.setFont(F_TEXTE);
		FontMetrics fm = g.getFontMetrics();
		// Hauteurs des caractères (au dessus et en dessous de la ligne de base)
		int ha = fm.getMaxAscent(), hb = fm.getMaxDescent();

		// Texte à afficher :
		String s = "fr.wikibooks.org";
		// Largeur du texte, et calcul de la position centrée du texte
		int ws = fm.stringWidth(s),
			x = (d.width-ws)/2, y = (d.height+ha-hb)/2;

		// Dégradé linéaire vertical débutant à 5 pixels du haut et se terminant à 5 pixels du bas du texte.
		GradientPaint gp = new GradientPaint(new Point(x,y-ha+5), C_FG_TEXT_1, new Point(x,y+hb-5), C_FG_TEXT_2);
		Paint p = g.getPaint(); // Pour restaurer
		g.setPaint(gp);
		g.drawString(s, x, y);
		g.setPaint(p); // Restauration
	}
}

Utiliser la forme des caractères

modifier

Pour un affichage de texte plus évolué, il est possible d'exploiter la forme des caractères. La forme des caractères est représentée par une instance de la classe java.awt.font.GlyphVector. Cette classe possède des méthodes retournant des instances de la classe java.awt.Shape qui peuvent donc être manipulées, et notamment utilisées avec les méthodes draw et fill du contexte graphique.

 
Contour d'un texte et effets utilisant la forme des caractères.

Le texte tel que montré par l'image ci-dessus est dessiné par le code ci-dessous et reprend la première partie du code de la section précédente, en remplaçant l'affichage du texte par la récupération de la forme des caractères. Celle-ci est obtenue en appelant la méthode createGlyphVector de la police de caractère, en lui passant le contexte de rendu et le texte.

Cette forme des caractères est ensuite utilisée :

  • pour obtenir les rectangles autour du caractère w (en position 3),
  • pour tracer le contour des glyphes du texte en pointillé.
package org.wikibooks.fr.test;

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;

import javax.swing.*;

/**
 * Test affichage en dégradé d'un texte.
 * @author fr.wikibooks.org
 */
public class TestPaint extends JComponent
{
	public static void main(String[] args)
	{
		JFrame f = new JFrame("Test de rendu avec Java Swing");
		f.setSize(800, 300);
		f.add(new TestPaint());
		f.setVisible(true);
	}

	/** Police de caractères utilisée. */
	private static final Font F_TEXTE = new Font("Dialog", Font.BOLD, 80);

	/** Couleurs utilisées. */
	private static final Color
		C_BG = Color.BLACK,
		C_LIGNE = new Color(210,160,40),
		C_RECT_1 = new Color(20,120,240),
		C_RECT_2 = new Color(0,64,128),
		C_TEXTE = Color.WHITE;

	/** Options pour améliorer l'affichage. */
	private HashMap<RenderingHints.Key, Object> hm_hints = new HashMap<RenderingHints.Key, Object>();

	private TestPaint()
	{
		hm_hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		hm_hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		hm_hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
		hm_hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
		setBackground(C_BG);
	}

	@Override
	protected void paintComponent(Graphics gg)
	{
		Graphics2D g = (Graphics2D) gg;
		g.setRenderingHints(hm_hints);

		// Selon les dimensions du composant
		Dimension d = getSize();
		g.setColor(getBackground());
		g.fillRect(0, 0, d.width, d.height);

		g.setFont(F_TEXTE);
		FontMetrics fm = g.getFontMetrics();
		// Hauteurs des caractères (au dessus et en dessous de la ligne de base)
		int ha = fm.getMaxAscent(), hb = fm.getMaxDescent();

		// Texte à afficher :
		String s = "fr.wikibooks.org";
		// Largeur du texte, et calcul de la position centrée du texte
		int ws = fm.stringWidth(s),
			x = (d.width-ws)/2, y = (d.height+ha-hb)/2;

		// Ensemble de glyphes du texte à partir de la police de caractères
		GlyphVector gv = F_TEXTE.createGlyphVector(g.getFontRenderContext(), s);
		Shape
			sh_title = gv.getOutline(), // Contour des caractères
			// Caractère en position 3 (w) :
			sh_w_ext = gv.getGlyphLogicalBounds(3), // - Rectangle de toute la zone du caractère
			sh_w     = gv.getGlyphVisualBounds(3);  // - Rectangle resserré sur le caractère

		g.setColor(C_LIGNE);
		g.fillRect(x-100, y-14, ws+200, 16);

		AffineTransform at = g.getTransform();
		g.translate(x, y);

		g.setColor(C_RECT_1);
		g.fill(sh_w_ext);
		g.setColor(C_RECT_2);
		g.fill(sh_w);

		// Tracé en pointillé, épaisseur 3 pixels
		Stroke sk = new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 5.0f, new float[]{ 10f, 5f }, 5f);
		Stroke st = g.getStroke();
		g.setStroke(sk);
		g.setColor(C_TEXTE);
		g.draw(sh_title);
		g.setStroke(st);    // Restaurer
		g.setTransform(at); // Restaurer

		// Équivalent de g.drawString :
		//g.drawGlyphVector(gv, x, y);
	}
}


Créer une image

Une application peut avoir besoin de créer une image, soit pour réutiliser cette image et l'afficher plusieurs fois en évitant de tout redessiner à nouveau, soit pour créer un fichier externe utilisable dans une autre application.

La création d'une image peut se faire aussi simplement que le dessin d'un composant, en utilisant un contexte graphique.

Créer une image

modifier

La classe java.awt.image.BufferedImage permet de créer une nouvelle image. Le type d'image et sa taille en pixels doit être spécifié au constructeur.

Exemple :

// Image 200x100 pixels
// Couleurs RVB sur 8 bits regroupés en pixels de type entier (int) :
BufferedImage b_image = new BufferedImage(200,100, BufferedImage.TYPE_INT_RGB);

Dessiner le contenu

modifier

Le contenu est ensuite dessiné en obtenant un contexte graphique, comme celui utilisé pour dessiner un composant :

Graphics2D g = b_image.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,200,100);
g.setColor(Color.BLUE);
g.drawRect(100,50,50,25);
g.dispose(); // Libérer le contexte après utilisation

Comme le contexte graphique est de la même classe que celui utilisé pour le dessin d'un composant, les mêmes méthodes s'appliquent, donc notamment la méthode addRenderingHints pour améliorer la qualité de l'image.

Enregistrer l'image

modifier

Enregistrer l'image se fait en utilisant la classe javax.imageio.ImageIO, en appelant la méthode statique write et en lui passant l'image, le nom du format d'image, et le chemin du fichier (de type java.io.File):

ImageIO.write(b_image, "png", new File("D:\Temp\image.png"));

Capture d'écran

modifier

La classe java.awt.Robot permet d'automatiser certaines actions de l'utilisateur (déplacer le curseur de souris, générer des touches, ...) et de réaliser des captures d'écrans. Son but principal est de pouvoir tester des applications automatiquement.

La classe possède deux constructeurs :

Robot()
Constructeur d'instance pour l'écran principal.
Robot(GraphicsDevice device)
Constructeur d'instance pour l'écran spécifié.

Ensuite, il suffit d'appeler la méthode createScreenCapture(Rectangle bounds) pour créer une image de capture de la zone spécifiée de l'écran. L'image retournée est de type java.awt.image.BufferedImage.

Le code suivant réalise une capture de l'écran principal :

Robot robot = new Robot();

MediaTracker mt = new MediaTracker(this); // Argument de type Component, 
//    this ok si le code est dans une classe de type composant ou fenêtre.

// Dimension(width, height) --> Rectangle(0, 0, width, height)
Rectangle tout_l_ecran = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());

BufferedImage image_capture = robot.createScreenCapture(tout_l_ecran);

mt.addImage(image_capture, 0); // Ajouter l'image à attendre
mt.waitForAll(); // Attendre que l'image soit prête

La capture est réalisée de manière asynchrone, ce qui explique l'utilisation de la classe java.awt.MediaTracker dans l'exemple précédent pour attendre que l'image soit prête à être utilisée.

  GFDL Vous avez la permission de copier, distribuer et/ou modifier ce document selon les termes de la licence de documentation libre GNU, version 1.2 ou plus récente publiée par la Free Software Foundation ; sans sections inaltérables, sans texte de première page de couverture et sans texte de dernière page de couverture.