Un jeu de données est composé de lignes et de colonnes ; une page PDF est une grille de coordonnées vierge dépourvue de ces deux concepts. Combler cet écart est tout le travail requis ici. Il n'y a pas d'appel DrawTable dans HotPDF acceptant un jeu de données et renvoyant une grille formatée. Ce que vous obtenez sont les éléments de base constituant la grille : TextOut pour placer une chaîne à un point donné, SetFont pour sélectionner la police, Rectangle et Fill pour colorer les bandes, et MoveTo/LineTo/Stroke pour tracer les lignes. Un exportateur de tableau utilisable consiste à traduire la pensée en lignes et colonnes en coordonnées x et y explicites, puis à maintenir la validité de ces coordonnées lorsque les données dépassent le bas de la page
L'exemple qui suit présente des fiches clients, mais rien dans le code de dessin ne sait ni ne se soucie d'où viennent les lignes. La version d'origine utilisait un TTable historique ; une requête FireDAC, un dataset en mémoire ou un simple tableau d'enregistrements alimente les mêmes routines sans changement. Ce qui compte, c'est de pouvoir parcourir les données une ligne à la fois et lire quatre champs chaîne dans chacune. Gardez le rendu séparé de la source de données et vous pourrez changer l'un ou l'autre sans déranger l'autre
Priorité à la géométrie de colonne
Déterminez la position de chaque colonne avant de dessiner le moindre caractère. Un tableau comporte ici quatre colonnes, ce qui nécessite quatre bords gauches et une marge droite connue. Coder en dur un nombre magique à chaque appel TextOut — comme le font souvent les exemples rapides — est précisément la cause des difficultés rencontrées lors de l'élargissement ultérieur du tableau. Nommez les bords une fois pour toutes en points depuis l'origine en haut à gauche, et chaque appel de dessin s'y référera par son nom :
const
ColNo = 70; // bord gauche de la colonne "No."
ColName = 110; // company name
ColAddr = 300; // street address
ColCity = 480; // city
RowLeft = 50; // table frame: left rule
RowRight = 570; // table frame: right rule
RowStep = 20; // distance verticale entre les lignes de base
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// Un bandeau teinté derrière la ligne. Rectangle prend le X, Y du coin
// supérieur gauche, puis Width, Height ; Y est le bord haut du texte de la ligne
Page.SetRGBFillColor($00FFF3DD);
Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
Page.Fill;
Page.SetRGBFillColor(clBlack);
end;
Page.TextOut(ColNo, Y, 0, ANo);
Page.TextOut(ColName, Y, 0, AName);
Page.TextOut(ColAddr, Y, 0, AAddr);
Page.TextOut(ColCity, Y, 0, ACity);
end;
Deux détails sont utiles ici. Les bandes de couleur sont dessinées en premier, puis le texte est tracé par-dessus, car l'ordre de dessin dans le PDF correspond à l'ordre sur l'axe Z : remplir un rectangle après le texte recouvrirait le contenu de la ligne. De plus, l'ombrage alterné n'est pas qu'une simple décoration. Dans les rapports denses, c'est le moyen le plus économique d'éviter que l'œil ne glisse sur la mauvaise ligne, c'est pourquoi la boucle bascule une variable booléenne à chaque ligne et la transmet directement à Shaded
Les positions de colonnes ci-dessus sont fixes, ce qui est honnête pour un rapport dont vous maîtrisez le schéma. Quand les données sont variables, mesurez au lieu de deviner. HotPDF expose la mesure de largeur de texte sur l'objet page, si bien que la version production de PrintRow peut prendre la valeur la plus longue attendue dans chaque colonne, la mesurer une fois à la taille de police choisie, et déduire les bords gauches de ces largeurs plus une gouttière. La forme de la routine ne change pas ; seule la source des constantes change
En-têtes, lignes et l'unique emplacement qui les détient
Une page A4 rendue sous forme de bitmap 32 bits à un niveau de zoom confortable pour la lecture représente environ plusieurs mégaoctets. Multipliez cela par un contrat de 400 pages, et ce nombre n'est plus abstrait : effectuer le rendu préalable de toutes les pages signifie demander à Windows une mémoire bitmap bien supérieure à 1 Go, alors que l'utilisateur ne consultera qu'un seul écran de contenu à la fois. L'application risque soit d'épuiser l'espace d'adressage dans un build 32 bits, soit de se bloquer complètement dans les premières secondes pendant que le GPU et l'analyseur de pages traitent des pages que personne ne fera jamais défiler. Un lecteur à défilement continu doit se présenter visuellement comme une longue bande de pages, mais en réalité, il est impossible de conserver toutes les pages en mémoire simultanément
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// À gauche : libellé de source et numéro de page. À droite : heure de génération
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Page ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Deux filets horizontaux qui encadrent les titres de colonnes
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// Les titres de colonnes, dans une graisse plus forte pour qu'ils se lisent comme des en-têtes
Page.SetFont('Times New Roman', [fsBold], 12);
Page.SetRGBFillColor(clNavy);
PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
Page.SetRGBFillColor(clBlack);
Y := Y + RowStep + 45; // on passe l'en-tête encadré avant la première ligne de corps
end;
Notez que DrawHeader transmet Y par référence et le déplace vers l'avant. L'appelant n'a jamais besoin de se souvenir de la hauteur de l'en-tête ; la routine qui le dessine est celle qui détient cette information. Cette règle de propriété unique garantit que la mise en page ne dérive pas lorsque vous ajoutez ultérieurement un logo ou un résumé de filtre à l'en-tête. La boucle principale l'ignore, se contentant de dessiner les lignes à partir de l'endroit où Y pointe actuellement
Les filets eux-mêmes font la différence entre une liste et un tableau. Les séparateurs verticaux de colonnes appliquent la même idée à l'axe x : un MoveTo / LineTo / Stroke à chaque bord de colonne, courant du filet supérieur jusqu'au bas de la dernière ligne de la page. L'exemple s'en tient aux filets horizontaux pour rester lisible, mais l'opération de production est mécanique dès que les constantes de colonnes existent
La boucle de curseur détient la logique de pagination
Dessiner est la moitié facile. La moitié qui sépare un jouet d'un rapport, c'est la pagination : savoir, avant de dessiner une ligne, si elle tient encore, et commencer une page neuve avec un en-tête neuf quand elle ne tient pas. Cette décision appartient à un seul endroit, la boucle qui parcourt les données, et nulle part ailleurs
var
Pdf: THotPDF;
Page: THPDFPage;
Y: Single;
PageNo: Integer;
Shaded: boolean;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'CustomerReport.pdf';
Pdf.BeginDoc;
Page := Pdf.CurrentPage;
// Titre du rapport, une seule fois, en haut de la première page
Page.SetFont('Arial', [fsBold], 24);
Page.TextOut(200, 40, 0, 'Customer Report');
PageNo := 1;
Y := 80;
DrawHeader(Page, Y, PageNo);
Shaded := False;
CustomerTable.First;
while not CustomerTable.Eof do
begin
// Plus de place ? On ouvre une nouvelle page et on y répète l'en-tête
if Y > 780 then // au-delà de la marge basse d'une page A4 de 842 pt
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage fait avancer CurrentPage
Inc(PageNo);
Y := 80;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont doit être réappliqué sur chaque nouvelle page
PrintRow(Page, Y,
VarToStr(CustomerTable['CustNo']),
VarToStr(CustomerTable['Company']),
VarToStr(CustomerTable['Addr1']),
VarToStr(CustomerTable['City']),
Shaded);
Y := Y + RowStep;
CustomerTable.Next;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Deux faits de coordonnées pilotent toute la boucle. HotPDF mesure y vers le bas depuis le coin supérieur gauche, comme un canvas VCL, alors même que le PDF qu'il écrit stocke les coordonnées de bas en haut ; les lignes avancent donc vers le bas de la page en ajoutant RowStep à Y à chaque tour, et le test de page pleine se déclenche quand Y dépasse la marge basse, ici 780 points sur la page A4 par défaut de 842 points. Inversez le sens — comme le fait le code porté depuis des coordonnées PDF brutes — et votre première ligne s'imprime près du bord bas pendant que la boucle croit avoir une page entière de place
Un autre fait sur lequel presque tout le monde trébuche une fois : AddPage crée une nouvelle page et redirige CurrentPage vers elle, mais elle n'emporte aucun contenu : aucune police, aucune couleur de remplissage, aucune position. C'est pourquoi il faut relire Page depuis CurrentPage après chaque AddPage, et pourquoi SetFont doit être réappelé avant la ligne principale. Omettez la relecture, et vous continuerez à dessiner sur la page que vous venez de quitter ; omettez la police, et la nouvelle page effectuera le rendu avec la police par défaut du visualiseur
Ce qui casse les exportateurs de tableaux
La plupart des bogues de tableaux ne se produisent pas sur le chemin normal de quelques lignes bien rangées. Ils existent aux limites, et les limites sont faciles à tester une fois que l'on sait où elles se trouvent
- Jeu de données vide : boucler sur zéro ligne produit une page contenant uniquement l'en-tête, ce qui semble au moins intentionnel. Une page blanche sans en-tête ressemble à un échec. Décidez de ce que vous souhaitez avant la publication
- Lignes tombant exactement sur la limite : générez un rapport dont la dernière ligne se trouve exactement un pas au-dessus de la marge, puis un autre rapport dont la ligne suivante se trouve exactement un pas en dessous. Un défaut de pagination d'une ligne reste caché jusqu'à ce que les données atteignent précisément la longueur critique
- Valeurs trop longues : un nom d'entreprise plus large que la colonne débordera sur la colonne suivante. Mesurez les champs et choisissez une stratégie : retour à la ligne sur une deuxième ligne, rognage ou troncature avec points de suspension. Le silence n'est pas une stratégie
- Champs vides : lire directement une valeur nulle dans
TextOutpeut afficher le texte littéralNullou un espace blanc, selon la manière dont vous le convertissez. Choisissez délibérément le mode de rendu plutôt que de laisser le variant décider à votre place
Avant de déclarer le travail terminé, exécutez le résultat dans plusieurs visualiseurs. Les comportements de substitution de polices et de rognage varient selon les moteurs de rendu, et un tableau propre dans un lecteur PDF peut afficher des colonnes mal alignées ou des textes tronqués dans un autre. Confirmez que la répétition des en-têtes, l'ombrage des lignes et les marges persistent après le franchissement de la limite de page, et que la numérotation reste continue lorsque les données débordent
Dessiner soi-même la grille au lieu de s'appuyer sur un concepteur de rapports visuels nécessite plus de code, mais le compromis mérite d'être explicité : vous contrôlez chaque coordonnée, ce qui est précisément requis pour les tâches par lots sur serveur, les factures et les exports d'audit — qui doivent s'afficher de manière identique sur chaque machine —, tandis que pour un simple listing interne, c'est un coût que vous préféreriez éviter. Pour les premiers, dès lors que le rapport en production doit correspondre exactement à ce que vous voyez sur votre bureau, le contrôle absolu en vaut la peine
Les filets et bandes ombrées ci-dessus s'appuient sur les mêmes primitives vectorielles et couleurs que celles couvertes par la présentation du dessin sur canevas, si vous voulez d'abord voir les appels Rectangle, MoveTo et LineTo traités pour eux-mêmes. Les primitives de dessin utilisées ici font partie du composant HotPDF pour Delphi et C++Builder