Dans cette étude, nous nous intéressons particulièrement au mode graphique, et ainsi nous allons voir comment réaliser des tracés en deux dimensions ainsi que du traitement sur les images. Les classes que nous utiliserons pour dessiner proviennent de six paquetages : java.awt, java.awt.color, java.awt.font, java.awt.geom, java.awt.image et java.awt.print. Ensemble, ces classes constituent l'essentiel de l'API 2D, destinée au tracé de formes, de texte et d'images.
Nous remarquons que ces paquetages sont tous issus de java.awt. Bien qu'actuellement, nous utilisons plutôt la bibliothèque graphique javax.swing, nous remarquons que l'ancienne bibliothèque java.awt peut s'avérer bien utile.
On appelle contexte graphique une instance de la classe java.awt.Graphics2D. Ce contexte graphique représente une surface de dessin, comme la zone d'affichage d'un composant, une page d'imprimante ou un tampon d'image invisible. Un contexte graphique fournit des méthodes pour réaliser trois sortes d'objets graphiques : formes, textes et images.
L'objet Graphics2D est baptisé contexte graphique car il contient également les informations contextuelles sur la zone de dessin, notamment une zone de masquage, la couleur du tracé, le mode de transfert, la police de caractères et la transformation géométrique de la zone de dessin. Si vous considérez la zone de dessin comme une toile, le contexte graphique sera un chevalet supportant un ensemble d'outils et se distinguant de la zone de dessin.
Depuis la version 1.0 du JDK, la classe Graphics disposait de méthodes pour dessiner des lignes, des rectangles, des ellipses, etc. Mais ces opérations de dessin sont très limitées. Par exemple, vous ne pouvez pas tracer des traits d'épaisseurs différentes ni faire pivoter les formes. Le JDK 1.2 a introduit la bibliothèque Java 2D qui implémente un jeu d'opérations graphiques très puissantes. Ce jeu d'opérations est implémenté par la classe Graphics2D.
Lorsque nous désirons réaliser des tracés ou travailler sur des images, cela sous-entend, bien entendu, que notre programme soit en mode graphique et que du coup notre fenêtre hérite de la classe JFrame. A partir de là, deux scénarii se présentent. Soit nous traçons directement sur la zone client de la fenêtre, ce qui est tout à fait possible, à condition de créer un contexte graphique. Attention, dans ce mode là, le tracé n'est effectué qu'une seule fois, ce qui sous-entend que le rafraîchissement du tracé n'est pas prévu. Effectivement, en imaginant que nous décidions de placer notre fenêtre momentanément en barre de tâche et qu'ensuite nous demandons de la réafficher, le tracé ne sera pas spécialement demandé puisque rien n'indique de le faire, et du coup le contenu de votre fenêtre sera vierge.
Il existe une méthode qui est spécialisée pour l'affichage des composants graphiques et qui prend donc en compte le rafraîchissement. Cette méthode est automatiquement sollicitée dès que nous avons besoin d'afficher ou de réafficher le composant. Cette méthode s'appelle paintComponent() pour les composants graphiques de la bibliothèque swing et paint() pour les composants issus de la bibliothèque awt.
Lorsque la fenêtre doit se réafficher, soit parce qu'elle se trouvait sur la barre des tâche, soit parce qu'elle se situait en dessous d'une autre fenêtre, le cadre de la fenêtre s'affiche en premier, et ensuite c'est le tour de chacun des composants qui constituent cette fenêtre, comme les boutons, les menus, les labels, les panneaux. C'est à ce moment là que la méthode paintComponent() de chacun de ces composants est sollicité. C'est à l'intérieur de cette dernière que se trouve tout le tracé correspondant au composant, comme par exemple le tracé d'un bouton.
Nous devons donc passer par cette méthode pour être sûr que notre tracé spécifique soit réaffiché au moment du rafraîchissement. Dans ce cas là, nous sommes donc obligés de redéfinir cette méthode. Du coup, cela impose de faire un héritage par rapport à un composant graphique existant. Ainsi, dans cette démarche, nous pouvons par exemple proposer un affichage spécifique sur un bouton. Toutefois, le composant que nous utilisons très fréquemment pour réaliser de nouveaux tracés est le panneau. Il a été justement conçu pour cela. Nous hériterons donc fréquemment de la classe JPanel qui représente ce panneau.
Nous pouvons également prendre la classe JComponent comme conteneur de tracés, qui est certe plus rudimentaire, mais qui du coup prend moins de ressources. Ce composant est en fait l'ancêtre de tous les composants graphiques. C'est justement à partir de cette classe qu'apparaît la méthode paintComponent().

| Fenêtre.java |
|---|
package dessin; import java.awt.*; import javax.swing.*; public class Fenêtre extends JFrame { public Fenêtre() { this.setDefaultCloseOperation(this.EXIT_ON_CLOSE); this.setSize(300, 250); this.setTitle("Test dessins"); this.getContentPane().setBackground(Color.ORANGE); this.getContentPane().add(new Zone()); } public static void main(String[] args) { new Fenêtre().setVisible(true); } } class Zone extends JComponent { protected void paintComponent(Graphics g) { g.drawArc(-40, 10, 100, 100, 0, 90); g.drawLine(10, 10, 10, 60); g.drawLine(10, 60, 60, 60); g.drawOval(10, 80, 120, 120); g.drawPolygon(new int[] {110, 170, 170}, new int[] {60, 60, 10}, 3); g.drawPolyline(new int[] {210, 270, 270}, new int[] {60, 60, 10}, 3); g.drawRect(10, 80, 120, 120); g.drawRoundRect(150, 80, 120, 120, 10, 10); } } |
Pour dessiner des formes dans la bibliothèque Java 2D, vous devez obtenir un objet de la classe Graphics2D. Il s'agit d'une classe fille de la classe Graphics. Effectivement, la méthode paintComponent() reçoit automatiquement en argument un objet de la classe Graphics2D. Toutefois, le paramètre de cette méthode est lui de type Graphics. Vous devez donc effectuer un transtypage dans le cas où vous désirez utiliser la technologie 2D.
public void paintComponent(Graphics g) { Graphics2D zone = (Graphics2D)g; ... }
Lorsqu'une fenêtre demande le rafraîchissement de chacun de ses composants, elle ne fait pas appel directement à la méthode paintComponent() mais passe par l'intermédiaire de la méthode repaint() - dont le nom est d'ailleurs assez explicite. Cette dernière s'occupera elle-même d'appeler, au moment opportun, la méthode paintComponent() . Au préalable, cette méthode repaint() doit s'occuper d'un certain nombre de choses pour que cela se passe dans de bonnes conditions, avec notamment la récupération du contexte graphique.
Il est a noté que nous pouvons avoir besoin de demander de rafraîchir (donc de réafficher) la zone de tracé sans attendre que la fenêtre s'en occupe. Dans ce cas là, il suffit de solliciter cette méthode repaint(). Il est même possible de réafficher qu'une petite partie de votre zone d'affichage. Il suffit alors de définir la zone à raffraîchir en passant un objet Rectangle en paramètre de la méthode repaint(). Seule cette zone rectangulaire sera alors réaffichée.
L'une des originalités de l'API 2D tient à ce que les formes, le texte et les images sont manipulés pour l'essentiel de la même façon. Le rendu est le procédé qui consiste à prendre un ensemble de formes, textes et images et à définir une façon de les représenter en colorant les pixels sur un écran ou une imprimante. Graphics2D gère quatre opérations de rendu :
Par rapport aux méthodes proposées par la classe Graphics, l'API Java 2D supporte plusieurs options supplémentaires :
Le contexte graphique instancié par un objet Graphics2D est constitué d'attributs ci-dessous, dont les valeurs sont contrôlées par diverses méthodes d'accès :
Naturellement, dans un certain nombre de circonstances pratiques, vous n'aurez pas besoin de passer par toutes ces étapes. Il existe en effet des choix par défaut suffisants pour un contexte graphique 2D classique. Vous devrez modifier ces paramètres uniquement si vous souhaitez modifier les valeurs par défaut.
Les diverses méthodes setXXX() définissent simplement l'état du contexte graphique 2D. Elles ne génèrent en aucun cas un dessin directement. De même, lorsque vous construisez des objets Shape, aucun dessin n'est effectué. Une forme est uniquement dessinée lorsque vous appelez draw() ou fill(). A ce moment précis, une nouvelle forme est calculée dans un pipeline d'affichage.
Les primitives graphiques (formes, texte et images) traversent le moteur de rendu suivant une série d'opérations baptisée pipeline d'affichage (ou de rendu). Dans ce pipeline d'affichage, les opérations sont ainsi réalisées suivant une séquence, donc dans un ordre bien précis. Finalement, les étapes suivantes sont nécessaires pour dessiner une forme :

Graphics2D comporte quelques méthodes permettant de dessiner et remplir des formes courantes ; elles sont en fait héritées de la classe Graphics.
| Méthode | Description |
|---|---|
| drawArc(int x, int y, int largeur, int hauteur, int angledébut, int anglefin)
|
Dessine un arc de cercle (angle en degré) |
| drawLine(int xdébut, int ydébut, int xfin, int yfin) |
Dessine une ligne |
| drawOval(int x, int y, int largeur, int hauteur) | Dessine un ovale |
| drawPolygon(int[] lesX, int[] lesY, int nombrePoint) | Dessine un polygone et le ferme en joignant les extrémités |
| drawPolyline(int[] lesX, int[] lesY, int nombrePoint) | Dessine une ligne en reliant une série de points, sans la fermer |
| drawRect(int x, int y, int largeur, int hauteur) | Dessine un rectangle |
| drawRoundRect(int x, int y, int largeur, int hauteur, int largeurArc, int hauteurArc) | Dessine un rectangle à coins arrondis |
| fillArc(int x, int y, int largeur, int hauteur, int angledébut, int anglefin) | Dessine un arc de cercle plein |
| fillOval(int x, int y, int largeur, int hauteur) | Dessine un ovale plein |
| fillPolygon(int[] lesX, int[] lesY, int nombrePoint) | Dessine un polygone plein |
| fillRect(int x, int y, int largeur, int hauteur) | Dessine un rectangle plein |
| fillRoundRect(int x, int y, int largeur, int hauteur, int largeurArc, int hauteurArc) | Dessine un rectangle plein à coins arrondis |
Pour chacune des méthodes fill() du tableau, il existe une méthode draw() correspondante créant la forme selon un dessin non plein. A l'exception de fillArc() et de fillPolygon(), chaque méthode prend une simple indication <x, y> correspondant au coin supérieur gauche de la forme, ainsi qu'une largeur width et une hauteur height.
La méthode la plus souple dessine un polygone, défini par deux tableaux de coordonnées des sommets, d'une part le tableau des x, et d'autre part le tableau des y suivi ensuite du nombre de point. Des méthodes de la classe Graphics lisent ces tableaux et dessinent le contour du polygone ou remplissent celui-ci.
La méthode fillArc() requiert six arguments entiers. Les quatre premiers définissent le rectangle englobant un ovale - comme la méthode fillOval(). Les deux derniers définissent la portion de l'ovale à dessiner, sous forme de position angulaire de départ et de déplacement (tous deux en degrés). La position zéro degré se trouve à trois heures ; l'angle croît dans le sens des aiguilles d'une montre.
| Fenêtre.java |
|---|
package dessin; import java.awt.*; import javax.swing.*; public class Fenêtre extends JFrame { public Fenêtre() { this.setDefaultCloseOperation(this.EXIT_ON_CLOSE); this.setSize(300, 250); this.setTitle("Test dessins"); this.getContentPane().setBackground(Color.ORANGE); this.getContentPane().add(new Zone()); } public static void main(String[] args) { new Fenêtre().setVisible(true); } } class Zone extends JComponent { protected void paintComponent(Graphics g) { g.drawArc(-40, 10, 100, 100, 0, 90); g.drawLine(10, 10, 10, 60); g.drawLine(10, 60, 60, 60); g.drawOval(10, 80, 120, 120); g.drawPolygon(new int[] {110, 170, 170}, new int[] {60, 60, 10}, 3); g.drawPolyline(new int[] {210, 270, 270}, new int[] {60, 60, 10}, 3); g.drawRect(10, 80, 120, 120); g.drawRoundRect(150, 80, 120, 120, 10, 10); } } |
Les méthodes que nous venons de voir font parties de la classe Graphics. L'API 2D se sert d'une approche entièrement différente, orientée objet. Au lieu de méthodes, il existe les classes correspondantes suivantes :
Ces classes implémentent toutes l'interface Shape.
Enfin, il existe une classe Point2D qui décrit un point avec des coordonnées x et y. Les points se révèlent utiles pour définir des formes, bien qu'ils n'en soient pas eux-mêmes.
Les classes Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D et Arc2D correspondent respectivement aux méthodes drawLine(), drawRect(), drawRoundRect(), drawOval() et drawArc(). L'API 2D rajoute deux classe supplémentaires : les courbes quadratiques et cubiques. Nous aborderons ces formes un peu plus loin. Il n'existe aucun classe Polygone2D. En remplacement, la classe GeneralPath décrit les tracés composés de lignes, de courbes quadratiques ou cubiques. Vous pouvez utiliser GeneralPath pour décrire un polygone.
Les classes Rectangle2D, RounRectangle2D, Ellipse2D, Arc2D héritent toute d'une superclasse commune, RectangleShape. Indubitablement, les ellipses les arcs ne sont pas des formes rectangulaires, mais ils peuvent être contenus dans un rectangle :

Pour dessiner une forme, il faut commencer par créer un objet d'une classe qui implémente l'interface Shape. Il suffit ensuite dans la classe Graphics2D de faire appel uniquement à l'une des deux méthodes spécifiques, soit la méthode draw() pour dessiner le contour de la forme, soit la méthode fill() si vous désirez remplir cette forme.

Voici le codage correspondant du tracé d'un rectangle :
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; Rectangle2D rectangle = new Rectangle2D.Double(10.0, 10.0, 200.0, 100.0); surface.draw(rectangle); } }
Avant l'apparition de la bibliothèque Java 2D, les programmeurs utilisaient les méthodes de la classe Graphics telles que drawRect() pour dessiner des formes. En apparence, les appels de méthode de l'ancien style paraissent plus simple. Cependant, avec la bibliothèque Java 2D, vos options restent ouvertent - vous pouvez ultérieurement améliorer vos dessins à l'aide des nombreux outils que fournit la bibliothèque (transformation, dégradés, transparence, composition entre les formes et les images, etc.).
L'utilisation des classes de formes Java 2D amène une certaine complexité. Contrairement aux méthodes de la version 1.0, qui utilisaient des entiers pour coordonnées de pixel, Java 2D emploie les valeurs de coordonnées en virgule flottante. Cela est souvent pratique, car vous pouvez spécifier vos formes des coordonnées qui sont significatives pour vous (comme des millimètres par exemple), puis les traduire en pixels.
La bibliothèque Java utilise des valeurs float simple précision pour nombre de ses calculs géométriques est de définir des pixels à l'écran ou sur l'imprimante. Après tout, le but ultime des calculs géométriques est de définir des pixels à l'écran ou sur l'imprimante. Tant que l'erreur d'arrondi reste cantoné à un pixel, l'aspect visuel n'est pas affecté. De plus, les calculs en virgule flottante sont plus rapides sur certaines plate-formes et les valeurs float requièrent un volume de stockage de moitié par rapport aux valeurs double.
Cependant, la manipulation de valeurs float est parfois peu pratique pour le programmeur, car le langage Java est inflexible en ce qui concerne les transtypages nécessaires pour la conversion de valeurs double en valeur float. Effectivement, l'expression suivante est une erreur :
float f = 1.2; // Erreur 1.2 est une contante littérale double
Cette instruction échoue à la compilation, car la constante 1.2 est de type double. Le remède consiste à ajouter le suffixe F à la constante :
float f = 1.2F;
De même, les instructions suivantes produisent le même type d'erreur :
Rectangle2D r = ...
float f =r.getWidth(); // Erreur
Cette instruction échouera également à la compilation, pour les même raisons. La méthode getWidth() renvoie un double. Cette fois-ci, le remède consiste à prévoir un transtypage :
Rectangle2D r = ...
float f =(float)r.getWidth(); // Erreur
Les suffixes et les transtypages étant un inconvénient évident, les concepteurs de la bibliothèque 2D ont décidé de fournir deux versions de chaque classe de forme : une avec des coordonnées float pour les programmeurs économes, et une avec des coordonnées double pour les paresseux (je suis de ceux là).
Les concepteurs de la bibliothèque ont à priori choisi une méthode curieuse, et assez déroutante au premier abord. En effet, chacune des classes dont le nom se termine par "2D" est en fait une classe abstraite qui contient deux sous-classes concrètes (classes internes statiques) qui héritent en même temps de cette même classe abstraite, et qui permettent de spécifier les coordonnées, soit sous forme de float, soit sous forme de double.

Ce n'est que lors de lors de la construction d'un objet 2D que vous devrez choisir entre un constructeur avec des coordonnées float ou double :
Rectangle2D floatRectangle = new Rectangle2D.Float(5.0F, 10.0F, 7.5F, 15.0F);
Rectangle2D doubleRectangle = new Rectangle2D.Double(5.0, 10.0, 7.5, 15.0);
Les classes Xxx2D.Float et Xxx2D.Double constituent des sous-classes des classes Xxx2D. Après la construction des objets, il n'existe aucun avantage à se souvenir de la sous-classe et vous pouvez simplement stocker l'objet construit dans une variable de superclasse, comme précédemment.
Comme vous pouvez le voir d'après ces noms curieux, les classes Xxx2D.Float et Xxx2D.Double sont également des classes internes des classes Xxx2D. Plus précisément, il existe deux classes internes Float et Double pour chacune des classes abstraites Xxx2D. Il ne s'agit que d'un côté pratique au niveau de la syntaxe, en vue d'éviter l'augmentation des noms des classes extérieures.
Il est préférable d'ignorer que les deux classes concrètes sont internes statiques, c'est juste une astuce pour éviter d'avoir à fournir des noms tels que FloatRectangle2D et DoubleRectangle2D.
Nous allons maintenant passer en revue l'ensemble des classes qui permettent de tracer des formes particulières.
Commençons tout d'abord par l'interface Shape qui dispose un certain nombre de méthodes qui sont impérativement définies dans les classes qui implémentent cette interface.
Toutes les classes concrètes qui implémentent Shape doivent impérativement redéfinir ces méthodes, puisque dans le cas d'une interface, elles sont justes déclarées. Nous imaginons bien, par exemple, que pour indiquer si un point fait parti de la zone interne d'une forme, ce n'est pas du tout la même chose entre un rectangle et une ellipse. Il faut donc que chaque classe propose le test adéquat.
Les classes comme Rectangle2D et Ellipse2D héritent toutes deux d'une superclasse commune RectangularShape. Bien sûr, comme nous l'avons déjà vus, les ellipses ne sont pas rectangulaires, mais elles sont incluses dans un rectangle englobant.

La classe RectangularShape définit plus de 20 méthodes qui sont communes à ces formes, parmi lesquelles les incontournables getWidth(), getHeight(), getCenterX(), getCenterY().

Les objets Rectangle2D et Ellipse2D sont simples à construire. Vous devez spécifier :
En voici le canevas :
Rectangle2D rectangle = new Rectangle2D.Double(x, y, largeur, hauteur);
Ellipse2D ellipse = new Ellipse2D.Double(x, y, largeur, hauteur);
Pour les ellipses, ces valeurs font référence au rectangle englobant. Par exemple :
Ellipse2D ellipse = new Ellipse2D.Double(150, 200, 100, 50);
construit une ellipse englobée dans un rectangle dont le coin supérieur gauche a les coordonnées (150, 200), avec une largeur de 100, et une hauteur de 50.
Il arrive que vous ne disposiez pas directement des valeurs du coin supérieur gauche. Il est assez fréquent d'avoir les deux coins opposés de la diagonale d'un rectangle. Vous devez d'abord créer un rectangle vide et utilisez ensuite la méthode setFrameFromDiagonal(), soit en passant les coordonnées du point haut gauche et du point bas droit, soit directement avec les points représentatifs en tant qu'objet Point2D :
Rectangle2D rectangle = new Rectangle2D.Double();
rectangle.setFrameFromDiagonal(xdébut, ydébut, xfin, yfin);
ou alors :
Rectangle2D rectangle = new Rectangle2D.Double();
...
Point2D début = new Point2D.Double(xdébut, ydébut);
Point2D fin = new Point2D.Double(xfin, yfin);
...
rectangle.setFrameFromDiagonal(début, fin);
Lors de la construction d'une ellipse, vous connaissez généralement le centre, la largeur et la hauteur, mais pas les popints des angles du rectangle englobant (qui ne repose pas sur les ellipses). Il existe une méthode setFrameFromeCenter() qui utilise le point central, malheureusement elle requiert toujours l'un des quatre points d'angle. Pour tracer la même ellipse vue plus haut, et en utilisant le centre comme critère, voici ce que nous devons coder :
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(200, 225, 250, 250);
Les méthodes setFrameFromDiagonal() et setFrameFromeCenter() sont en fait des méthodes de RectangularShape. Vous pouvez donc les utiliser pour n'importe quelle classe fille, donc aussi bien pour Rectangle2D que pour Ellipse2D.
Voici un exemple traitant des méthodes que nous venons d'évoquer :
class Zone extends JComponent { private Ellipse2D ellipse = new Ellipse2D.Double(); private Rectangle2D rectangle = new Rectangle2D.Double(); public Zone() { Rectangle2D r = new Rectangle2D.Double(50, 50, 190, 110); ellipse.setFrameFromCenter(r.getCenterX(), r.getCenterY(), r.getMaxX(), r.getMaxY()); rectangle.setFrameFromDiagonal(ellipse.getX(), ellipse.getY(), r.getMaxX(), r.getMaxY()); } protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; surface.draw(rectangle); surface.draw(ellipse); } }
Cet exemple sert à montrer l'utilisation des méthodes qui peuvent être intéressantes dans certaines applications. Ici, nous aurions pu, bien entendu, créer l'ellipse et le rectangle directement àau moment de l'appel des constructeurs en spécifiant les coordonnées x et y avec la largeur et la hauteur.
La classe RoundRectangle2D permet de tracer des rectangles avec les coins arrondis. Pour cette forme, vous devez aussi spécifier les coordonnées du coin supérieur gauche, la largeur, la hauteur ainsi que les dimensions en x et en y de la zone qui doit être arrondie.

RoundRectangle2D rectangle = new RoundRectangle2D.Double(x, y, largeur, hauteur, largeurArc, hauteurArc);
La classe qui implémente l'arc de cercle est la classe Arc2D. Pour construire un arc, il faut fournir un rectangle entourant cet arc, suivi de l'angle de départ et de l'angle d'ouverture, ainsi que du type de fermeture, qui peut être Arc2D.OPEN, Arc2D.PIE ou Arc2D.CHORD.


Attention, les angles s'expriment en dégré.
.
Arc2D arc = new Arc2D.Double(x, y, largeur, hauteur, angleDépart, angleArc, typeFermetureArc);
Voici un exemple de demi-cercle dont l'arc de départ est fixé à 60°.class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; Arc2D arc = new Arc2D.Double(10, 10, 200, 200, 60, 180, Arc2D.PIE); surface.draw(arc); } }
Ici, nous pouvons choisir comme type de fermeture aussi bien Arc2D.PIE que Arc2D.CHORD.
Dans ce chapitre, nous allons voir comment tracer des lignes, des courbes quadratiques et cubiques ainsi qu'un assemblage de ces différents types de tracé.
le tracé des segments de lignes s'obtient au moyen de la classe Ligne2D. Pour construire une ligne, vous fournissez les points de départ et d'arrivé, sous la forme d'objets Point2D ou de paires de nombres.

Point2D début = new Point2D.Double(xdébut, ydébut);
Point2D fin = new Point2D.Double(xfin, yfin);
...
Ligne2D ligne = new Ligne2D.Double(début, fin);
ou directement :
Ligne2D ligne = new Ligne2D.Double(xdébut, ydébut, xfin, yfin);
La bibliothèque Java 2D fournit des courbes quadratiques représentées par la classe QuadCurve2D et des courbes cubiques représentées par la classe CubicCurve2D. Les courbes quadratiques et cubiques sont spécifiées par deux points de terminaison, et un ou deux points de contrôle. C'est d'ailleurs le nombre de point de contrôle qui détermine le type de courbe. Ainsi, la forme des courbes peut être modifié en changeant les points de contrôle.


QuadCurve2D q = new QuadCurve2D.Double(xdébut, ydébut, contrôleX, contrôleY, xfin, yfin);
CubicCurve2D c = new CubicCurve2D.Double(xdébut, ydébut, contrôleDébutX, contrôleDébutY, contrôleFinX, contrôleFinY, xfin, yfin);
Les courbes quadratiques ne sont pas très flexibles, et elles ne sont pas utilisées très souvent. Les courbes cubiques (courbes de Bezier) sont bien plus répandues. En combinant plus courbes cubiques, de sorte que les tangentes soient respectées aux points de connexion, vous pouvez créer des formes arrondies complexes.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170); surface.draw(pétale); } }
Il est possible de construire des séquences arbitraires de segments de lignes, de courbes quadratiques et de courbes cubiques, et de les enregistrer dans un objet GeneralPath. Voici la procédure à suivre :
Pour créer un polygone, appelez simplement moveTo() pour aller au premier angle, suivi d'appels répétés lineTo() pour parcourir les autres points. Pour terminer, appeler closePath() pour fermer le polygone.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath triangle = new GeneralPath(); triangle.moveTo(10, 10); triangle.lineTo(10, 100); triangle.lineTo(100, 100); triangle.closePath(); surface.draw(triangle); } }
Un tracé général n'a pas besoin d'être connecté. Vous pouvez appeler moveTo() à n'importe quel moment pour commencer un nouveau segment de tracé.
.
Pour terminer, vous pouvez avoir recours à la méthode append() pour ajouter des objets Shape à un tracé général. Le contour de la forme est alors ajouté à la fin du tracé. Le second paramètre de la méthode append() est true si la nouvelle forme doit être connectée au premier point du tracé, ou false dans le cas contraire.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath dessin = new GeneralPath(); dessin.moveTo(150, 140); dessin.curveTo(10, 0, 290, 0, 150, 140); dessin.append(new Ellipse2D.Double(120, 140, 60, 60), false); surface.draw(dessin); } }
Dans la section précédente, vous avez vu comment générer des formes complexes en construisant des tracés généraux composés de lignes et de courbes. En utilisant un nombre suffisant de lignes et de courbes, vous pouvez représenter quasiment n'importe quelle forme. Par exemple, les formes des caractères des polices visibles sur un écran ou sur un papier imprimé sont toutes composées de lignes et de courbes cubiques.
Cependant, et de manière occasionnelle, il est plus simple de décrire une courbe en la décomposant en zones, comme des rectangles, des polygones ou des ellipses. L'API Java 2D dispose d'un objet de zone représenté par la classe Area. Cette classe supporte quatre opérations de combinaisons de zones géométriques, qui mélangent les pixels de deux zones pour former une nouvelle zone :

Pour construire une zone complexe, il faut commencer avec un objet de zone par défaut (vierge). Puis, vous pouvez combiner cette zone avec n'importe quelle autre forme. La classe Area implémente l'interface Shape. Vous pouvez alors dessiner contours de zone avec la méthode draw(), ou en remplir l'intérieur avec la méthode fill() de la classe Graphics2D.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath dessin = new GeneralPath(); dessin.moveTo(150, 140); dessin.curveTo(10, 0, 290, 0, 150, 140); dessin.append(new Ellipse2D.Double(120, 140, 60, 60), false); Area zone = new Area(); zone.add(new Area(new Rectangle2D.Double(70, 20, 160, 190))); zone.subtract(new Area(dessin)); surface.fill(zone); } }
L'opération draw() de la classe Graphics2D dessine le contour d'une forme en utilisant l'outil de dessin courant. Par défaut, cet outil est une ligne pleine d'un seul pixel d'épaisseur. Vous pouvez sélectionner un autre outil en appelant la méthode setStroke(). Il suffit de fournir un objet d'une classe qui implémente l'interface Stroke. En fait, l'API Java 2D définit une seule classe adaptée, il s'agit de BasicStroke.
Ainsi, nous pouvons construire des outils de dessin de n'importe quel épaisseur :
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath dessin = new GeneralPath(); dessin.moveTo(180, 30); dessin.lineTo(105, 105); dessin.moveTo(30, 30); dessin.lineTo(30, 180); dessin.lineTo(180, 180); dessin.closePath(); surface.setStroke(new BasicStroke(20)); surface.draw(dessin); } }
Lorsqu'un outil fait plus d'un pixel de large, son extrémité peut prendre différents aspects :

Lorsque deux tracés épais se croisent, il existe trois possibilités pour le style de l'intersection :

L'intersection angulaire n'est pas adaptée aux lignes qui se croisent selon un angle aigu. Si deux lignes se croisent selon un angle inférieur à la limite angulaire, une intersection enbiais est choisie automatiquement en remplacement. Cela permet d'éviter les prolongements angulaires trop longs. Par défaut, cette limite angulaire est fixée à dix degrés.
Pour spécifier les extrémités et les intersections, vous devez utiliser encore une fois la méthode setStroke(). Vous passez également en argument de cette méthode un objet de type BasicStroke, mais cette fois-ci en construisant l'objet à l'aide de trois ou quatre arguments :
surface.setStroke(new BasicStroke(épaisseur, extrémité, intersection, limite angulaire (en option)));
En reprenant le graphisme de tout à l'heure, et si nous désirons avoir un tracé plutôt arrondi, voilà comment procéder :
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath dessin = new GeneralPath(); dessin.moveTo(180, 30); dessin.lineTo(105, 105); dessin.moveTo(30, 30); dessin.lineTo(30, 180); dessin.lineTo(180, 180); dessin.closePath(); surface.setStroke(new BasicStroke(20, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); surface.draw(dessin); } }
Pour terminer, vous pouvez choisir des lignes pointillées en définissant un motif de remplissage. Un motif de remplissage est composé d'un tableau float[] de nombres contenant la longueur des tirets blancs et noirs.
![]()
Le motif de remplissage et la phase de remplissage peuvent être spécifiés lors de la construction du BasicStroke. Toutefois, cette partie s'intègre à la suite de la définition des extrémités et des intersections de traits. La phase de remplissage indique la position de démarrage de chaque ligne par rapport au motif de remplissage. Normalement cette valeur vaut 0.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; GeneralPath dessin = new GeneralPath(); dessin.moveTo(180, 30); dessin.lineTo(105, 105); dessin.moveTo(30, 30); dessin.lineTo(30, 180); dessin.lineTo(180, 180); dessin.closePath(); float[] motif = {5, 10, 5, 10, 20, 10}; surface.setStroke( new BasicStroke(5, // épaisseur BasicStroke.CAP_ROUND, // extrémité BasicStroke.JOIN_MITER, // intersection 15, // limite angulaire motif, // motif de remplissage 0 // phase de remplissage )); surface.draw(dessin); } }
Lorsqu'une forme est remplie, son intérieur est recouvert d'une couleur de remplissage. La méthode setPaint() permet de choisir un style de remplissage parmi plusieurs objets qui implémentent l'interface Paint. Dans l'API Java 2D, il existe trois classes adéquates suivant le type de remplissage voulu :
Ces types de remplissages peuvent tout aussi bien être utilisés pour l'apparence du trait (du contour) que pour l'apparence du fond de la forme choisie. Suivant le cas, il suffit alors de faire appel, soit à la méthode draw(), soit à la méthode fill() de la classe Graphics2D.
La méthode setPaint() de la classe Graphics2D permet de sélectionner une couleur qui sera employée par toutes les opérations de dessin ultérieures pour le contexte graphique. Pour dessiner avec plusieurs couleurs, effectuer une opération, puis sélectionner une autre couleur avant de procéder à l'opération suivante.
Les couleurs sont définies à l'aide de la classe Color. La classe java.awt.Color propose des constantes prédéfinies pour les treize couleurs standard indiqué dans le tableau ci-dessous.
| Constantes prédéfinies | Couleur standard correspondante |
|---|---|
Color.black |
noir bleu cyan (bleu clair) gris foncé gris vert gris clair magenta orange rose rouge blanc jaune |
surface.setPaint(Color.red);
Vous pouvez spécifier une couleur personnalisée en créant un objet Color à l'aide de ses composantes rouge, verte et bleue. En utilisant une échelle de 0 à 255 (chaque composante est codée sur un octet) pour les proportions de rouge, de vert et de bleu, appelez le constructeur de Color de la façon suivante :
surface.setPaint(new Color(0, 128, 128)); // un bleu-vert foncé
Voici un exemple de définition de couleur à la fois sur le trait ainsi que sur le fond des formes constituant le tracé de l'application :
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170); surface.setPaint(Color.YELLOW); surface.fill(pétale); Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60); surface.setStroke(new BasicStroke(10)); surface.draw(cercle); surface.setPaint(new Color(255, 128, 0)); surface.fill(cercle); } }
Les méthodes brighter() et darker() de la classe Color produisent des versions plus vives ou plus foncées de la couleur actuelle. La méthode brighter() permet de mettre un élément en surbrillance, mais avive en réalité à peine la couleur. Pour obtenir une couleur plus visible, appelez plusieurs fois la méthode.
couleur.brighter().brighter().brighter();
Un dégradé de couleurs est un passage graduel d'une couleur à une autre. La classe GradientPaint encapsule ce concept dans une implémentation d'interface Paint. Il nous suffit d'indiquer deux points de la couleur de chaque point. GradientPaint s'occupe de tous les détails pour faire fondre progressivement la couleur d'un point à l'autre.
Les objets GradientPaint peuvent être construits en fournissant deux points et les couleurs de ces deux points :
surface.setPaint(new GradientPaint(premierPoint, couleurPremierPoint, deuxièmePoint, couleurDeuxièmePoint));
Les couleurs sont interpolées le long de la ligne joignant ces deux points et elles sont constantes le long des lignes perpendiculaires à cette ligne. Les points situés après le dernier point de cette ligne prennent la couleur du dernier point de cette ligne.
Sinon, vous pouvez appeler le constructeur GradientPaint (avec un paramètre supplémentaire) en choisissant true pour le paramètre cyclique :
surface.setPaint(new GradientPaint(premierPoint, couleurPremierPoint, deuxièmePoint, couleurDeuxièmePoint, cyclique));
Le dernier paramètre de GradientPaint définit donc si le gradient est cyclique. Dans un gradient cyclique, les couleurs continuent de fluctuer au-delà des deux points indiqués. Sinon (si la valeur est false), le gradient dessine un mélange unique d'un point à l'autre. Au-delà de chaque point extrême, la couleur est unie.
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170); surface.setPaint(new GradientPaint( 10, 10, // ou alors un seul paramètre new Point2D.Double(10, 10), Color.red, 30, 30, // ou alors un seul paramètre new Point2D.Double(30, 30), Color.yellow, true )); surface.fill(pétale); Ellipse2D cercle = new Ellipse2D.Double(120, 140, 60, 60); surface.setStroke(new BasicStroke(10)); surface.draw(cercle); surface.setPaint(new GradientPaint( 120, 140, // ou alors un seul paramètre new Point2D.Double(120, 140), Color.blue, 180, 200, // ou alors un seul paramètre new Point2D.Double(180, 200), Color.cyan )); surface.fill(cercle); } }
Une texture est une image continuellement répétée, comme un carrelage. Ce concept est représenté dans l'API 2D par la classe TexturePaint. Pour créer une texture, il suffit d'indiquer l'image à utiliser et le rectangle qui servira à la reproduire.
Vous avez vu comment créer des images simples en traçant des lignes et des formes. Des images complexes, telles que des photographies, sont habituellement générées en externe, par exemple avec un scanner ou un logiciel dédié à la manipulation d'images. Il est également possible de produire une image pixel par pixel, et de stocker le résultat dans un tableau.
Pour construire un objet TexturePaint, il faut spécifier une BufferedImage et un rectangle d'origine. Ce rectangle est prolongé indéfiniment sur les deux axes (x et y) pour remplir entièrement le plan. L'image est agrandie ou retrécie pour remplir exactement le rectangle d'origine, puis reproduite dans tous les autres rectangles.
Vous pouvez créer un objet BufferedImage en fournissant la taille de l'image et son type. Le type le plus répandu est BufferedImage.TYPE_INT_ARGB, dans lequel chaque pixel est spécifié par un entier décrivant les valeurs de rouge, de vert, de bleu et de transparence (ou alpha)
BufferedImage image = new BufferedImage(largeur, hauteur, BufferedImage.TYPE_INT_ARGB);
Ensuite, vous obtenez un contexte graphique vous permettant de dessiner dans votre image :
Graphics2D zoneImage = image.createGraphics();
Toutes les opérations de dessin dans zoneImage ont maintenant pour effet de remplir l'image intermédiaire avec des pixels. Lorsque vous avez fini, vous pouvez créer votre objet TexturePaint :
surface.setPaint(new TexturePaint(image, rectangle));
Voici un exemple où nous fabriquons la texture de toute pièce :
class Zone extends JComponent { protected void paintComponent(Graphics g) { Graphics2D surface = (Graphics2D) g; CubicCurve2D pétale = new CubicCurve2D.Double(150, 170, 10, 10, 290, 10, 150, 170); BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); Graphics2D zoneImage = image.createGraphics(); zoneImage.setPaint(Color.red); zoneImage.draw(new Ellipse2D.Double(0, 0, 10, 20)); Rectangle2D rectangle = new Rectangle2D.Double(0, 0, image.getWidth(), image.getHeight()); surface.setPaint(new TexturePaint(image, rectangle)); surface.fill(pétale); Ellipse2D ellipse = new Ellipse2D.Double(120, 140, 60, 60); surface.setStroke(new BasicStroke(30)); surface.draw(ellipse); surface.setPaint(new GradientPaint( 120, 140, // ou alors un seul paramètre new Point2D.Double(120, 140), Color.blue, 180, 200, // ou alors un seul paramètre new Point2D.Double(180, 200), Color.cyan )); surface.fill(ellipse); } }
Dans cet exemple, nous avons choisi de prendre un rectangle dont la dimension correspond à la taille de l'image. Rien n'empêche de choisir des dimensions du rectangle qui soit un multiple de la taille de l'image, par exemple, deux fois la largeur sur trois fois la hauteur.
Il est possible également de récupérer une texture depuis un fichier image sur le disque dur. La classe ImageIO est spécialement conçue pour cela et simplifie la lecture d'un fichier graphique dans une image intermédiaire.
BufferedImage image = ImageIO.read(new File("fichier image.gif"));
Tout comme le tracé du contour d'une forme, celui du texte n'est qu'une variante du remplissage d'une forme. Lorsque nous demandons à un Graphics2D de dessiner un texte, il détermine les formes nécessaires au dessin et les remplit. Les formes représentant des caractères sont baptisées Glyphes. Une police est une collection de glyphes.
Pour dessiner un texte, nous faisons appel à la méthode drawString() de Graphics2D qui existait déjà dans la classe mère Graphics, mais qui a été redéfinie afin de permettre les différents traitements (remplissage, tranformations, clipping, etc.) déjà utilisés par les formes (Shape) plus classiques.
surface.drawString("Bienvenue ! ", 50, 150));
Lorsque nous appelons drawString(), Graphics2D utilise la police courante pour récupérer les glyphes correspondant aux caractères de la chaîne. Puis les glyphes (qui ne sont pas des Shape) sont remplis à l'aide du Paint en cours.
Graphics2D.drawString(texte à afficher , x, y));
Il est possible d'écrire un texte avec une police différente que celle prévue par défaut. Les polices de caractères, appelées aussi fontes, sont représentées par des objets de la classe java.awt.Font. Un objet Font est construit à partir d'un nom de police, un identificateur de style et d'une taille.
Font police = new Font(nom de police, style, taille);
Il existe trois sortes de noms de polices : les familles, les caractères (également baptisés noms de polices) et les noms logiques. Les noms de famille et de police sont étroitement liés. Par exemple, "Garamond Italic" est une police de la famille "Garamond".
Un nom logique est un nom générique attribué à la famille. Les noms des polices logiques ci-dessous sont en général disponibles sur toutes les plate-formes :
Le nom logique est apparié à une police présente sur la plate-forme locale. Les fichiers fonts.properties de Java font correspondre les noms de polices aux polices disponibles, en recouvrant le plus de caractères Unicode possible. Si nous demandons une police inexistante, nous obtenons la police par défaut. L'une des plus belles avancées de l'API 2D est quelle sait utiliser la plupart des polices installées sur l'ordinateur.
Pour écrire des caractères d'une fonte, vous devez donc d'abord créer un objet de la classe Font. Vous spécifier alors la chaîne de caractère correspondant au nom de la fonte, le style de la fonte ainsi que sa taille.
Font verdana = new Font("Verdana", Font.BOLD, 14);
Le troisième paramètre est la taille en points. Le point est souvent utilisé en typ