Héritage et typographie
Le but de ce TD est de se familiariser avec la notion d’héritage. Nous allons construire progressivement des classes organisées selon l’arbre d’héritage illustré ci-dessous :
Ces classes représentent des éléments de typographie.
Note pour les utilisateurs d’Eclipse. Dans ce TD
nous allons utiliser les bibliothèques Java AWT afin de composer des
élements graphiques. Pour faciliter l’accès à ces bibliothèques dans
Eclipse, il vaut mieux créer un projet sans fichier module-info.java. Si
vous avez un fichier module-info.java et que vous ne souhaitez pas le
supprimer, il sera probablement nécessaire de lui ajouter
requires java.desktop;.
Tests automatiques. Pour les fonctions qui font un
affichage graphique, le serveur ne vérifie pas si le dessin est bon.
Cependant, au même endroit où vous trouvez habituellement les messages
d’erreur, vous aurez accès à un lien artifacts qui vous
permettra de visualiser côte à côte votre rendu avec un rendu de
référence.
Typographie
Un document typographié est obtenu en assemblant divers éléments
s’apparentant à des boîtes (il seront représentés dans ce TD par des
instances de la classe Box). Certaines de ces boîtes
représentent simplement un unique signe typographique, comme un
caractère ; on les appelle des glyphes (classe
Glyph). D’autres boîtes représentent des espaces (classe
Space) qui peuvent être fixes (espaces horizontaux entre
les lettres d’un même mot ; classe FixedSpace) ou étirables
(espaces entre mots si l’on veut un alignement des lignes qui soit
justifié ; classe RelativeSpace). Enfin, d’autres boîtes
représentent des empilements (classe Group) horizontaux
(classe Hbox) ou verticaux (classe Vbox) de
boîtes de façon à pouvoir construire des lignes ou des paragraphes.
À titre d’exemple, le resultat suivant (auquel vous devriez arriver en fin de TD) est un empilement vertical de 4 empilements horizontaux. Le premier empilement horizontal est composé de 17 glypes (L, ‘, h, o, m, m, e, n,’, e, s, t, q, u, ’, u, n), 2 espaces étirables et 14 espaces fixes.
Une boîte est caractérisée par trois grandeurs : sa largeur, sa
hauteur au dessus de la ligne de base (la ligne sur laquelle on écrit le
texte) et sa profondeur en dessous de cette même ligne de base. En
notant width, ascent et descent
ces trois grandeurs, on peut schématiser ainsi une boîte typographique
(la ligne de base étant représentée en pointillés) :
En outre, une quatrième grandeur, stretchingCapacity,
indique la possibilité, plus ou moins grande, d’étirer la largeur d’une
boîte. Si une boîte n’est pas étirable (par exemple, pour une espace
fixe), cette quantité est nulle.
Les longueurs width, ascent et
descent sont exprimées en points et
stretchingCapacity est un coefficient. Toutes ces grandeurs
seront des flottants (type double).
Paquet
La hiérarchie de classes que vous allez définir sera réunie au sein
d’un même paquet nommé typo. Il faut donc commencer par
créer le répertoire éponyme qui contiendra les fichiers de ces
classes.
Héritage
Boîte
Comme point de départ, créer une classe abstraite Box
sur le modèle suivant. Les deux paquets importés seront nécessaires par
la suite (question 3).
package typo;
import java.awt.Color;
import java.awt.Graphics;
public abstract class Box {
// vide pour l'instant
}Cette classe abstraite permettra de déclarer les opérations communes à tous les types de boîte (même si la façon de définir concrètement les opérations dans les sous-classes variera selon les cas).
Question 1
Ajouter des méthodes publiques abstraites getWidth,
getAscent, getDescent et
getStretchingCapacity dans la classe Box.
Glyphe
Le premier type d’éléments typographiques que nous allons coder sont
les glyphes. Créer une classe Glyph héritant de
Box contenant le code suivant pour les représenter.
package typo;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
public class Glyph extends Box {
final private static FontRenderContext frc
= new FontRenderContext(null, false, false);
final private Font font;
final private char[] chars;
final private Rectangle2D bounds;
// classe à compléter (question 2)
public String toString() {
return String.format("Glyph(%s)[w=%g, a=%g, d=%g, sC=%g]",
chars[0], this.getWidth(), this.getAscent(),
this.getDescent(), this.getStretchingCapacity());
}
}Ne vous souciez pas de la déclaration de l’attribut frc
pour le moment. La méthode toString() d’un objet
obj qui n’est pas de type String est appelée
par défault par System.out.print (ou ses variantes). Ainsi,
si g est un glyphe, on pourra afficher la chaîne de
caractères g.toString() dans la console à l’aide de la
commande System.out.println(g). Cet affichage textuel est
utile pour contrôler facilement ce qui est fait.
Question 2
Compléter la classe Glyph de la façon suivante :
Écrire un constructeur prenant en argument une police de caractères
font(classejava.awt.Font) et un caractèrec(typechar). On stockera le caractèrecdans le tableauchars, qui sera de taille 1 (ceci afin de faciliter l’utilisation dedrawCharsdans la question suivante).Les dimensions
boundsdu glyphe (qui sont déterminées par le choix de la policefontet du caractèrec) peuvent être obtenues à l’aide du code suivant :TextLayout layout = new TextLayout("" + chars[0], font, frc); bounds = layout.getBounds();Définissez l’implementation de la méthode
getStretchingCapacityrenvoyant 0 (par définition, la capacité d’étirement d’un glyphe est nulle).Définissez l’implementation des méthodes
getWidth,getAscentetgetDescentrenvoyant les dimensions du glyphe. Elles sont obtenues ainsi :// ascent = - bounds.getY() // descent = bounds.getHeight() + bounds.getY() // width = bounds.getWidth();
Pour tester, on pourra créer une classe Test dans le
package par défaut (le parent de typo) et y copier le code
suivant (à appeler depuis main) :
static void test2() {
Font f = new Font("SansSerif", Font.PLAIN, 70);
Glyph g = new Glyph(f, 'g');
System.out.println(g);
}qui doit donner la sortie suivante (les valeurs peuvent légèrement différer) :
Glyph(g)[w=32.0, a=37.125, d=14.734375, sC=0.0]
Dessin
Nous allons maintenant afficher les résultats à l’aide d’une fenêtre
graphique. Pour ce faire, on fournit une classe Page à ajouter au package par
défaut, à côté de Test. Vous n’avez pas besoin de lire son
code. Pour que cette classe soit utilisable, plusieurs modifications
doivent être apportées aux classes Box et
Glyph.
Question 3
Ajouter le code suivant dans la classe
Box:final static boolean DEBUG = false; public final boolean draw(Graphics graph, double x, double y, double w) { if (DEBUG) { graph.setColor(Color.red); graph.drawRect((int) x, (int) y, (int) w, (int) (getAscent() + getDescent())); graph.setColor(Color.black); } return doDraw(graph, x, y, w); }Remarquez la présence de la variable
DEBUG. La mettre temporairement àtruepourra être très utile pour déboguer votre code dans les questions suivantes.Ajouter une méthode
public abstract boolean doDraw(Graphics graph, double x, double y, double w)dans la classeBox. Quelque soit le type de boîte, cette méthode dessinera cette boîte avec son coin supérieur gauche aux coordonnéesx,ydans un rectangle de largeurw. On remarque que les coordonnées Y croissent vers le bas. Le booléen retourné est vrai si et seulement si aucun problème n’a été détecté (il ne pourra être faux que dans le cas des boîtes horizontales, à la question 7). Sa définition concrète sera effectuée dans les sous-classes en fonction du type de boîte à réaliser.Implémentez la méthode
doDrawdans la classeGlyph. Elle doit dessiner le caractère du glyphe à l’emplacementx,y. Pour cela, vous pourrez utiliser la méthodegraph.drawChars, après avoir sélectionné la police avec la méthodegraph.setFont. Les coordonnées à passer àgraph.drawCharssontx-bounds.getX(), y-bounds.getY(). En effet, on veut aligner le coin supérieur gauche du glyphe sur les coordonnéesx,ymaisdrawCharsattend les coordonnées où placer le point d’origine du glyphe. Comme tout est bien fait,getXetgetYdonnent les coordonnées du coin supérieur gauche du glyphe relativement à son point d’origine.
On pourra tester avec le code suivant :
static void test3() {
Font f = new Font("SansSerif", Font.PLAIN, 70);
Glyph g = new Glyph(f, 'g');
System.out.println(g);
new Page(g, 150, 150);
}qui doit donner la même sortie que précédemment et l’image suivante :
Factorisation
Pour contrôler ce que nous faisons, nous avons choisi d’afficher dans
la console les caractéristiques des glyphes sous la forme
Glyph(g)[w=32.0, a=37.125, d=14.734375, sC=0.0]. Nous
allons maintenant considerer d’autres sous-classes de Box
et nous afficherons toujours dans la console leurs caractéristiques sous
une forme similaire. Pour gagner en temps et en praticité, nous allons
factoriser le code commun aux fonctions toString
de toutes les sous-classes de Box.
Question 4
Ajouter une méthode
toStringà la classeBox, qui renvoie la partie entre crochets (inclus) de la chaîne, c’est-à-dire[w=32.0, a=37.125, d=14.734375, sC=0.0]dans l’exemple précédent.Simplifier ensuite le code de la méthode
toStringdeGlyphen utilisantsuper.toString.
On pourra réutiliser test3 pour tester.
Espace
Les espaces peuvent être soit des espaces fixes (entre deux caractères d’un même mot), soit des espaces étirables (entre les mots d’une ligne justifiée).
Question 5
Définir une classe
Spacehéritant deBoxpour représenter une espace ayant comme attributs une largeurwidthet une capacité d’étirementstretchingCapacity, tout deux de typedouble. Quelle doit être la visibilité de ces attributs ?Définir un constructeur pour la classe
Spaceprenant en arguments une dimension minimale et une capacité d’étirement. Définir la méthodedoDrawcomme ne dessinant rien (il n’y a rien à dessiner pour représenter une espace) et les méthodesgetAscentetgetDescentcomme retournant 0. Redéfinir aussi la méthodetoStringpour indiquer qu’il s’agit d’une espace, en renvoyant la chaîneSpace[w=...]et en utilisant les informations fournies parsuper.toString.Définir ensuite deux sous-classes
FixedSpaceetRelativeSpacedeSpace, représentant respectivement une espace fixe et une espace proportionnelle à la taille d’une police de caractères. Le constructeur deFixedSpaceprendra une dimension de typedoubleen argument. Le constructeur deRelativeSpaceprendra un coefficientcde typedoubleet une police de caractèresfde typeFont, pour construire une espace de dimensionc * f.getSize()et de capacité d’étirement 1. Ces deux constructeurs feront appel au constructeur de la classeSpace, avec la syntaxesuper(...);. On ne demande pas de redéfinirtoStringdans ces deux classes.Écrire une méthode
test5dans la classeTest, qui construit trois objets dans les classesSpace,FixedSpaceetRelativeSpace, et les affiche avecSystem.out.println. On doit obtenir quelque chose comme suit:
Space[w=2.0, a=0.0, d=0.0, sC=3.0]
Space[w=5.0, a=0.0, d=0.0, sC=0.0]
Space[w=35.0, a=0.0, d=0.0, sC=1.0]Groupe
Les groupes sont des boîtes “conteneurs” permettant d’empiler
horizontalement (Hbox) ou verticalement (Vbox)
des boîtes.
Note : par définition, un groupe est une structure inductive.
Question 6
- Définir une classe abstraite
Grouphéritant deBoxpour représenter un empilement de boîtes. Cette classe va être utilisée pour factoriser autant que possible le code commun aux deux types d’empilements (horizontaux ou verticaux). On pourra utiliser uneLinkedList(du packagejava.util) pour représenter un empilement comme suit :
protected final LinkedList<Box> list = new LinkedList<Box>();Note : LinkedList<E> est une classe
générique qui implemente des listes chaînées d’éléments de type
E. L’élément <E> est un
placeholder pour la classe réelle qui est utilisée. Différentes
classes peuvent être utilisées pour créer différents types de noeuds.
Par exemple, dans TD3:4 vous avez créé la classe WordList
qui implémente une liste chaînée de String et la classe
EntryList qui implémente une liste chaînée de
Entry. Au lieu de ces classes vous auriez pu utiliser
LinkedList<String> et
LinkedList<Entry>.
Étendre la classe
Groupavec une méthodeaddqui permet d’ajouter une nouvelle boîte à la fin de leur liste.Définir les méthodes
getWidth,getAscent,getDescentetgetStretchingCapacityde la classeGroupde telle sorte qu’elles renvoient les valeurs mémorisées dans les quatre champsascent,descent,widthetstretchingCapacity.
Note : à ce point, on ne peut pas encore calculer les valeurs de ces champs car elles dépendent du fait que les espaces soient fixes ou étirables.
- Redéfinir la méthode
toStringde la classeGrouppour afficher un groupe sous la forme suivante.
[w=...]{
boite 1,
...
boite n,
}On rappelle qu’on peut parcourir les éléments de la liste
list avec la syntaxe for (Box b: list) .... On
rappelle également qu’on peut insérer un retour-chariot dans une chaîne
de caractères avec la syntaxe \n. On pourra également
utiliser la méthode replaceAll(str1,str2) de la classe
String permettant de remplacer chaque occurence de
str1 par str2 dans une chaîne de
caractères.
On veillera à ce que les indentations de boite 1, ..., boite n soient conservées, de sorte à obtenir par exemple :
[w=...]{
[w=...]{
boite 1.1,
...
boite 1.n,
},
...
boite n,
}- Écrire une classe
TestableGrouphéritant de la classe groupe et définissant une méthodedoDrawne dessinant rien. Ajouter ensuite une méthodetest6dans la classeTestqui produit quelque chose comme
[w=0.0, a=0.0, d=0.0, sC=0.0]{
[w=0.0, a=0.0, d=0.0, sC=0.0]{
Space[w=2.0, a=0.0, d=0.0, sC=3.0],
Space[w=5.0, a=0.0, d=0.0, sC=0.0],
},
Space[w=35.0, a=0.0, d=0.0, sC=1.0],
}Boîte horizontale
Une boîte horizontale est un groupe représentant un empilement horizontal de boîtes (ordonnées de la gauche vers la droite). Toutes les composantes partagent la même ligne de base. Ainsi, une boîte horizontale contenant trois boîtes ressemble à ceci (où la boîte horizontale est matérialisée en rouge) :
On définit la capacité d’étirement de la boîte comme la somme des capacités d’étirement de ses composantes.
Note : Pour vous familiariser avec l’héritage, nous vous
demandons de ne pas utiliser instanceof dans ce TD. Les
propriétés de la redéfinition de fonction sont parfaitement adaptées à
ce sujet.
Question 7
Définir la classe
Hboxhéritant deGroupet représentant les boîtes horizontales.Redéfinir la méthode
addpour qu’elle remplisse la liste avecsuper.addet mette à jour les quatre champsascent,descent,widthetstretchingCapacity.Redéfinir la méthode
toStringpour indiquer qu’il s’agit d’une boîte horizontale.Écrire la méthode
doDrawpermettant de dessiner une boîte horizontale. Elle procède de la manière suivante. Le dernier paramètre dedoDraw,w, spécifie la largeur désirée. La largeur minimale est obtenue pargetWidth; appelons-lamw. Simw > walors la boîte ne peut pas tenir dans la largeurw. On la dessine alors tout de même (sur une largeurmw), maisdoDrawretourne alors le booléenfalse. Si en revanchew >= mw, alors il n’y a pas de problème et on va répartir la différencew-mwsur toutes les espaces étirables contenues à l’intérieur de la boîte horizontale, proportionnellement à la capacité d’étirement de chacun. Si par exemple la boîte contient deux espaces de capacités 1 et 2 respectivement, alors un tiers de l’espace supplémentaire sera donné au premier et deux tiers au second. Comme on a justement attribué aux glyphes une capacité d’étirement 0, le traitement peut être fait de manière uniforme, sans avoir à connaître la nature de chaque composante.On pourra tester cette classe avec le code suivant
static void test7a() {
Hbox h = new Hbox();
System.out.println(h);
Font f = new Font("SansSerif", Font.PLAIN, 40);
h.add(new Glyph(f, 'a'));
System.out.println(h);
h.add(new Space(2., 3.));
System.out.println(h);
}qui doit donner une sortie de la forme
Hbox[w=0.0, a=0.0, d=0.0, sC=0.0]{
}
Hbox[w=19.09375, a=21.21875, d=0.46875, sC=0.0]{
Glyph(a)[w=19.09375, a=21.21875, d=0.46875, sC=0.0],
}
Hbox[w=21.09375, a=21.21875, d=0.46875, sC=3.0]{
Glyph(a)[w=19.09375, a=21.21875, d=0.46875, sC=0.0],
Space[w=2.0, a=0.0, d=0.0, sC=3.0],
}On pourra ensuite la tester avec le code suivant
static Hbox lineFromString(Font f, String s) {
Hbox line = new Hbox();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == ' ')
line.add(new RelativeSpace(0.5, f));
else {
line.add(new Glyph(f, c));
if (i < s.length() - 1 && s.charAt(i+1)!=' ')
line.add(new FixedSpace(2));
}
}
return line;
}
static void test7b() {
Font f = new Font("SansSerif", Font.PLAIN, 40);
Box t = lineFromString(f, "Typographie sans peine");
System.out.println(t);
new Page(t, 450, 150);
}qui doit donner une sortie de la forme
Hbox[w=410.359375, a=28.640625, d=8.421875, sC=2.0]{
Glyph(T)[w=22.703125, a=28.640625, d=0.0, sC=0.0],
Space[w=2.0, a=0.0, d=0.0, sC=0.0],
Glyph(y)[w=19.015625, a=20.75, d=8.421875, sC=0.0],
...et le résultat suivant :
Enfin, ce dernier test peut révéler des erreurs d’arithmétique :
static void test7c() {
Font f = new Font("SansSerif", Font.PLAIN, 40);
Box t = lineFromString(f, "Test");
System.out.println(t);
new Page(t, 450, 150);
}Boîte verticale
Une boîte verticale est un groupe représentant un empilement vertical de boîtes (ordonnées du haut vers le bas). Les différentes boîtes qu’elle contient sont séparées par un interligne. Par définition, la ligne de base d’une boîte verticale est celle de sa boîte la plus basse. Ainsi, une boîte verticale contenant trois boîtes ressemble à ceci (où la boîte verticale est matérialisée en rouge) :
On définit la capacité d’étirement de la boîte verticale comme le maximum des capacités d’étirement de ses composantes.
Question 8
Définir une classe
Vboxhéritant de la classeGroupet représentant un empilement vertical de boîtes. Ajouter un constructeur prenant en argument un interlignelineSkipde typedouble.Redéfinir la méthode
addpour qu’elle remplisse la liste avecsuper.addet mette à jour les quatre champsascent,descent,widthetstretchingCapacity.Redéfinir la méthode
toStringpour indiquer qu’il s’agit d’une boîte verticale.Écrire la méthode
doDrawpermettant de dessiner une boîte verticale. Pour cela, il suffit de dessiner les boîtes les unes au dessus des autres, en partant du haut et en espaçant les boîtes de la dimension indiquée parlineSkip. On rappelle que les coordonnées Y croissent vers le bas.Tester votre programme avec le code suivant
final static Box hfill = new Space(0, Double.POSITIVE_INFINITY);
static Vbox fromString(Font f, String s) {
Vbox text = new Vbox(5);
String[] lines = s.split("\n");
for (int i = 0; i < lines.length; ++i) {
Hbox line = lineFromString(f, lines[i]);
if (i+1 == lines.length)
line.add(hfill);
text.add(line);
}
return text;
}
static void test8a() {
Font f = new Font("SansSerif", Font.PLAIN, 40);
Box t = fromString(f,
"L'homme n'est qu'un\n" +
"roseau, le plus faible\n" +
"de la nature ; mais\n" +
"c'est un roseau pensant.");
new Page(t, 450);
}qui doit donner le résultat suivant :
ou encore avec le code suivant qui dessine une lettrine :
static void test8b() {
Font f = new Font("SansSerif", Font.PLAIN, 30);
Font lettrinef = new Font("SansSerif", Font.PLAIN, 120);
Vbox t = new Vbox(5);
Hbox h = new Hbox();
h.add(new Glyph(lettrinef, 'L'));
h.add(new Space(3, 1));
Vbox l = new Vbox(5);
l.add(lineFromString(f, "'homme n'est qu'un roseau, le"));
l.add(lineFromString(f, "plus faible de la nature ; mais"));
l.add(lineFromString(f, "c'est un roseau pensant. Il ne"));
h.add(l);
t.add(h);
t.add(lineFromString(f, "faut pas que l'univers entier s'arme"));
t.add(lineFromString(f, "pour l'écraser : une vapeur, une"));
t.add(fromString(f, "goutte d'eau, suffit pour le tuer."));
new Page(t, 500);
}qui doit ressembler à ceci :