Héritage et typographie
par Jean-Christophe Filliâtre

 Login :  Mot de passe :

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.

Avant de commencer

Pour les travaux dirigés du cours INF431, il est recommandé d'utiliser l'environnement de programmation Eclipse. Pour commencer, il faut définir un espace de travail (ou workspace), nommé par exemple INF431. Dans cet espace de travail, on va créer un projet pour chaque séance de TD, aujourd'hui ce sera par exemple TD2.

Typographie

Un document typographié est obtenu en assemblant divers éléments s'apparentant à des boîtes. Certaines de ces boîtes représentent simplement un unique signe typographique, comme un caractère ; on les appelle des glyphes. D'autres boîtes représentent des espaces qui peuvent être plus ou moins étirables. Enfin, d'autres boîtes représentent des empilements horizontaux ou verticaux de boîtes.

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) :

Pour une boîte représentant un espace étirable, width représente la longueur minimale de l'espace et une autre grandeur, stretchingCapacity, indique la possibilité, plus ou moins grande, d'étirer cet espace.

Les longueurs width, ascent et descent sont exprimées en points et stretchingCapacity est un coefficient. Toutes ces grandeurs seront des flottants (type double).

Héritage

On placera toutes les classes dans un package typo, à l'exception de la classe Test.

Comme point de départ, on se donne une classe abstraite Box, vide pour l'instant :

package typo;

import java.awt.Color;
import java.awt.Graphics;

public abstract class Box {
  // vide pour l'instant
}

Glyphe

On introduit une classe Glyph pour représenter un glyphe, héritant de Box.
package typo;

import java.awt.Font;

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

  public String toString() {
    return "Glyph(" + chars[0] + ")" + "[mW=" + getMinimalWidth() + ",a=" + getAscent() + ",d=" + getDescent() + ",sC=" + getStretchingCapacity() + "]";
  }
}
Pour compléter la classe Glyph : On pourra tester avec le code suivant (à mettre dans une classe Test dans le package par défaut et à appeler depuis main) :
static void test1() {
    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)[mW=33.53125,a=37.9375,d=14.328125,sC=0.0]
Déposer ici votre fichier Glyph.java :

Dessin

Pour dessiner, on fournit une classe Page, à mettre dans le package par défaut. Pour que cette classe soit utilisable, plusieurs modifications doivent être apportées aux classes Box et Glyph. On pourra tester avec le code suivant :
static void test2() {
    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 :

Déposer ici votre fichier Box.java :

Déposer ici votre fichier Glyph.java :

Factorisation

Afin de factoriser le code commun aux fonctions toString de toutes les sous-classes de Box, ajouter une méthode toString à la classe Box, qui renvoie la chaîne [mW=...].

Simplifier ensuite le code de la méthode toString de Glyph en utilisant super.toString.

On pourra réutiliser test2 pour tester.

Déposer ici votre fichier Box.java :

Déposer ici votre fichier Glyph.java :

Espace étirable

Définir une classe Space héritant de Box pour représenter un espace étirable, avec un constructeur prenant en arguments une dimension minimale et une capacité d'étirement. La méthode doDraw ne fait rien ; les méthodes getAscent et getDescent renvoient 0. Redéfinir la méthode toString pour indiquer qu'il s'agit d'un espace, en renvoyant la chaîne Space[mW=...] et en utilisant les informations fournies par super.toString.

Définir ensuite deux sous-classes FixedSpace et RelativeSpace de Space, représentant respectivement un espace non étirable et un espace proportionnel à la taille d'une police de caractères. Le constructeur de FixedSpace prendra une dimension de type double en argument. Le constructeur de RelativeSpace prendra un coefficient c de type double et une police de caractères f de type Font, pour construire un espace de dimension c * f.getSize() et de capacité d'étirement 1. Ces deux constructeurs feront appel au constructeur de la classe Space, avec la syntaxe super(...);. On ne demande pas de redéfinir toString dans ces deux classes.

Écrire une méthode test4 dans la classe Test, qui construit trois objets dans les classes Space, FixedSpace et RelativeSpace, et les affiche avec System.out.println. On doit obtenir quelque chose comme

Space[mW=2.0,a=0.0,d=0.0,sC=3.0]
Space[mW=5.0,a=0.0,d=0.0,sC=0.0]
Space[mW=35.0,a=0.0,d=0.0,sC=1.0]

Déposer ici votre fichier Space.java :

Déposer ici votre fichier FixedSpace.java :

Déposer ici votre fichier RelativeSpace.java :

Déposer ici votre fichier Test.java :

Groupe

Pour factoriser autant que possible le code des empilements horizontaux et verticaux de boîtes, définir tout d'abord une classe Group héritant de Box pour représenter une liste ordonnée de boîtes. On pourra utiliser une LinkedList (du package java.util) pour cela, ainsi :
protected final LinkedList<Box> list = new LinkedList<Box>();
Écrire pour les objets de la classe Group une méthode add qui permet d'ajouter une nouvelle boîte à la fin de leur liste.

Note : à ce point, on ne peut pas encore calculer les valeurs renvoyées par les méthodes getMinimalWidth, getAscent, getDescent et getStretchingCapacity. Plutôt que les laisser encore abstraites, on va écrire des méthodes qui renvoient les valeurs mémorisées dans quatre champs ascent, descent, minimalWidth et stretchingCapacity (tous protected), qui seront affectés depuis les sous-classes.

Redéfinir la méthode toString pour afficher le groupe sous la forme

[mw=...]{
  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.

Écrire une méthode test5 dans la classe Test qui produit quelque chose comme

[mW=0.0,a=0.0,d=0.0,sC=0.0]{
  Space[mW=2.0,a=0.0,d=0.0,sC=3.0],
  Space[mW=5.0,a=0.0,d=0.0,sC=0.0],
  Space[mW=35.0,a=0.0,d=0.0,sC=1.0],
}

Déposer ici vos fichiers Group.java et Test.java :

Note : On peut choisir de faire de la classe Group une classe abstraite (à ce point, on ne sait pas encore comment un groupe doit être dessiné). Dans ce cas, il faut déclarer doDraw comme abstraite et supprimer le test5 qui ne servait qu'à tester la méthode toString et qu'il n'est plus possible d'écrire maintenant.

Boîte horizontale

Définir une classe Hbox héritant de Group pour représenter 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.

Redéfinir la méthode add pour qu'elle remplisse la liste avec super.add et mette à jour les quatre champs ascent, descent, minimalWidth et stretchingCapacity.

Écrire la méthode doDraw. Le dessin d'une boîte horizontale (méthode doDraw) se fait de la manière suivante. Le dernier paramètre de doDraw, w, spécifie la largeur désirée. La largeur minimale est obtenue par getMinimalWidth ; appelons-la mw. Si mw>w alors la boîte ne peut pas tenir dans la largeur w, ce que l'on signale par un message sur la console, tout en la dessinant tout de même (sur une largeur mw). Si en revanche w>=mw alors on va répartir la différence w-mw sur tous les espaces étirables contenus à 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.

Redéfinir la méthode toString pour indiquer qu'il s'agit d'une boîte horizontale.

On pourra tester avec le code suivant

static void test6a() {
    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[mW=0.0,a=0.0,d=0.0,sC=0.0]{
}
Hbox[mW=18.46875,a=22.40625,d=0.5625,sC=0.0]{
  Glyph(a)[mW=18.46875,a=22.40625,d=0.5625,sC=0.0],
}
Hbox[mW=20.46875,a=22.40625,d=0.5625,sC=3.0]{
  Glyph(a)[mW=18.46875,a=22.40625,d=0.5625,sC=0.0],
  Space[mW=2.0,a=0.0,d=0.0,sC=3.0],
}

On pourra tester ensuite 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)
                line.add(new FixedSpace(2));
        }
    }
    return line;
}

static void test6b() {
    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[mW=410.046875,a=30.84375,d=8.1875,sC=2.0]{
  Glyph(T)[mW=24.609375,a=28.90625,d=0.0,sC=0.0],
  Space[mW=2.0,a=0.0,d=0.0,sC=0.0],
  ...
et le résultat suivant :

Ce dernier test peut révéler des erreurs d'arithmétique :

static void test6c() {
  Font f = new Font("SansSerif", Font.PLAIN, 40);
  Box t = lineFromString(f, "Test");
  System.out.println(t);
  new Page(t, 450, 150);
}

Déposer ici votre fichier Hbox.java :

Boîte verticale

Définir une classe Vbox héritant de la classe Group pour représenter un empilement vertical de boîtes (ordonnées de haut en bas). Le constructeur de Vbox prendra en argument un interligne lineSkip de type double. 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 :

On définit la capacité d'étirement de la boîte verticale comme le maximum des capacités d'étirement de ses composantes.

Comme pour Hbox, redéfinir la méthode add pour qu'elle remplisse la liste avec super.add et mette à jour les quatre champs ascent, descent, minimalWidth et stretchingCapacity.

Écrire la méthode doDraw. Le dessin d'une boîte verticale (méthode doDraw) se fait tout simplement en dessinant 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 par lineSkip. On rappelle que les coordonnées Y croissent vers le bas.

Redéfinir la méthode toString pour indiquer qu'il s'agit d'une boîte verticale.

On pourra tester 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);
    int len = s.length();
    for (int i = 0; i < len; ) {
        int idx = s.indexOf('\n', i);
        if (idx == -1) idx = len;
        Hbox line = lineFromString(f, s.substring(i, idx));
        if (idx == len) line.add(hfill); // ajoute un ressort infini à la fin de la dernière ligne
        text.add(line);
        i = idx+1;
    }
    return text;
}

static void test7a() {
    Font f = new Font("SansSerif", Font.PLAIN, 40);
    Box t = fromString(f, "L'homme n'est qu'un\nroseau, le plus faible\nde la nature ; mais\nc'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 test7b() {
    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 :

Déposer ici votre fichier Vbox.java :