L'impression avec Delphi

Jean-Luc Mellet(Alphomega)

« Comment imprimer facilement avec Delphi ? »
« Comment imprimer une grille de données ? »
« Comment redéfinir l'imprimante par défaut ? »
« Comment connaître la zone non imprimable ? »

- Titre : L'impression avec Delphi

- Auteur : Jean-Luc Mellet(Alphomega)

- Parution : 26 avril 2002

- Mise à jour : 27 décembre 2019

- Licence : Ce document est issu de https://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- Préambule

- I. TCanvas et Printer (épisode 1)

- II. L'impression avec Delphi (épisode 2)

- II-A. GetDc

- II-B. GetDeviceCaps

- II-C. DrawText

- II-D. CreateFont (ou CreateFontIndirect)

- II-E. TabbedTextOut

- III. Impression avec Delphi (épisode 3)

- IV. L'aperçu avant impression (épisode 4)

Préambule

On a souvent posé des questions sur l'impression de document avec Delphi:

« Comment imprimer facilement avec Delphi ? »
« Comment imprimer une grille de données ? »
« Comment redéfinir l'imprimante par défaut ? »
« Comment connaître la zone non imprimable ? »
etc ….

Voici donc une petite série d'articles pour aider les nouveaux utilisateurs de ce langage ! (et pourquoi pas les autres). Pour éviter une indigestion au lecteur, j'ai voulu séparer ce tutoriel en 4 leçons distinctes. Je parlerai tout d'abord de la base avec l'unité Printers, ensuite de quelques API utilises à l'impression, puis de ce que l'on peut faire en réunissant tout ça. Enfin, je termirai avec une technique pour créer un aperçu avant impression, qui pose souvent des problèmes aux développeurs débutants.

- Episode 1: Les objets TCanvas et Printer (pour les très débutants).

- Episode 2: Utilisation des API sur l'imprimante

- Episode 3: Que faire avec tout ça ?

- Episode 4: L'aperçu avant impression!

Avant de rentrer dans le vif du sujet, il convient de préciser que je vais présenter ici ce que j'appelle un mode d'impression « lignes à lignes » qui gère la création et la gestion de chaque élément de notre impression. Il existe bien sur des outils et composants « tout prèt » pour réaliser des impressions faciles. On peut ainsi trouver des outils de reporting (Générateurs d'états) ainsi que des composants qui prennent en charge une grande partie du travail de composition des pages. Un que j'affectionne particulièrement s'appelle TMWPrintObject et a été développé par James « Woody » Woodard. N'hésitez pas à lui réclamer (en anglais) la dernière version avec l'exemple qui l'accompagne, c'est en freeware et très bien fait. Pensez juste à lui dire que je vous ai parlé de lui, il verra que j'ai tenu parole :-) .

I - TCanvas et Printer (épisode 1)

La première chose à dire sur le sujet est que l'impression avec Delphi est des plus simple. Tout passe par une instance unique de la classeTPrinter qui s'obtient par la fonction Printer de l'unité Printers. L'unicité de Printer est nécessaire à cause de son utilisation intensive des API Windows (de façon transparente pour l'utilisateur). Ce modèle est une possibilité d'implémentation de la Design Pattern connue sous le nom de singleton. Voici un extrait de l'unité Printers !

function Printer: TPrinter;
begin
if FPrinter = nil then FPrinter := TPrinter.Create;
Result := FPrinter;
end;

finalization
FPrinter.free;  {libération automatique dans la partie finalization}
end;

Puisque Printer est en fait une fonction de l'unité Printers, vous y avez accès directement dans votre code. On ne crèe pas un objet Printer comme on crée un objet courant par sa méthode create. Le simple ajoût de l'unité Printers dans la clause uses de votre code vous donne accès à Printer. Alors, comment l'utiliser ? On demande à Printer de commencer une impression, on envoie les commandes désirées et on termine l'impression. C'est aussi simple que cela! Sous forme de code, cela donne:

Printer.BeginDoc;
Printer.TextOut(50, 50, 'Ce que je veux imprimer');
// .....
Printer.EndDoc;

Vous connaissez plus simple ? Moi non ! Bien sur, on ne peut se contenter de cela.Voyons maintenant les propriétés les plus importantes de Printer.

Comme vous le savez, Windows permet l'installation de plusieurs imprimantes. Pour connaitre la liste des imprimantes installées sur l'ordinateur, les utilisateurs de certains langages doivent utiliser des API du style EnumFont et des procédures de callback. Avec Delphi, rien de tout ça! Les imprimantes installées sont répertoriées dans la propriété Printers (TStrings). Ainsi, pour afficher cette liste dans un composant du type TListBox, on placera le composant TListBox sur la feuille et on écrira:

ListBox1.Items := Printer.Printers;

Vous me direz: « C'est bien d'avoir la liste, mais comment savoir quelle imprimante est utilisée actuellement par défaut et comment choisir dans mon code celle qui m'intéresse pour une impression bien définie! »

A cela je répondrai: PrinterIndex! La valeur de PrinterIndex indique l'imprimante par défaut pour windows. Donc, pour connaitre le nom de l'imprimante, il suffira de d'écrire:

ImprimanteParDefaut := Printer.Printers[Printer.PrinterIndex];

Pour utiliser une imprimante de la liste dans votre code, il suffit de modifier la valeur de PrinterIndex en lui affectant l'index correspondant dans la liste. On écrira par exemple:

Printer.PrinterIndex := 1;

Note: Pour revenir à l'imprimante par défaut de Windows, on affectera à à PrinterIndex la valeur -1 !

Bien ! Cette base étant établie, nous allons voir maintenant les propriétés PageHeight et PageWidth. Vous l'aurez compris, elles nous fournissent la hauteur et la largeur de la feuille à imprimer, définies par le choix du format de papier utilisé (A3, A4, etc ..).

PageHeight et PageWidth renvoie des valeurs en pixels. Il faut savoir que l'objet Printer ne connait QUE les pixels. Inutile de lui parler en millimètres ou en pouce ! Comment sont définies les valeurs de PageHeight et PageWidth ? Tout simplement par utilisation de l'API GetDeviceCaps (que nous utiliserons plus tard).

{Vous n'avez pas à utiliser cette API. Printer le fait pour vous}
PageHeight := GetDeviceCaps(Printer.handle, HORZRES);

Mais ça, c'est Printer qui s'en charge. Vous vous contentez de lui demander le résultat. Chose intéressante, c'est que les valeurs tiennent compte des zones non imprimables sur la feuille. Selon l'imprimante utilisée, une certaine zone en bordure de feuille n'est pas accessible à la tète d'impression. PageWidth et PageHeight fournissant la largeur et la hauteur de la zone imprimable, il peut-être intéressant de connaitre la taille de cette zone. Prenons par exemple le cas d'une impression de texte qui devra toujours débuter à 5 centimètres du bord gauche de la feuille, et ce, quelle que soit l'imprimante utilisée. Il faudra alors tenir compte de cette partie plus ou moins grande. Là, nous utiliserons aussi GetDeviceCaps. Pour le détail du calcul, je vous renvoie à 'Coordonnées imprimante en millimètres' où je décris le calcul à effectuer.

La dernière propriété que nous allons voir est la plus importante à mes yeux, puisque c'est elle qui va faire 95% du travail d'impression. Je veux parler du canvas. Le Canvas représente la surface d'impression de la page. Comme pour un composant TImage, ou le Canvas d'une TForm, l'objet Printer implémente un Canvas. Voici ce que dit l'aide Delphi sur l'objet TCanvas.

TCanvas propose des propriétés, événements et méthodes qui simplifient la création d'image pour :
Spécifier le type de pinceau, de crayon et de fonte à utiliser
Dessiner et remplir diverses formes et lignes
Ecrire du texte
Restituer des images graphiques
Définir la réponse aux modifications de l'image en cours.

C'est donc sur ce canvas que nous enverrons nos commandes. En fait, nous allons utiliser ce canvas exactement comme le Canvas d'un composant TImage (ce qui nous sera très utile dans l'épisode 4 quand nous parlerons de l'aperçu avant impression).

Nous allons terminer ce premier épisode pour débutant en écrivant quelques lignes de codes. Pour cela, nous allons ajouter à notre palette les méthodes TextWidth et TextHeight du canvas. Elles renvoient respectivement la largeur et la hauteur du texte passé en paramètre.

Commencez par créer un nouveau projet et ajouter l'unité Printers à la clause uses de votre code.

Placer ensuite sur votre feuille un TListBox que nous appelerons Imprimantes ainsi que 2 boutons de commande Lister et Episode1

Dans l'événement OnCLick du bouton Lister, écrivez le code suivant pour afficher la liste des imprimantes installées.

// Lister les imprimantes installées
Imprimantes.Items := Printer.Printers;
// Sélectionner l'imprimante définie dans Windows
Imprimantes.ItemIndex := Printer.PrinterIndex;

Dans l'événement OnClick d'Imprimantes, donnons nous le moyen de choisir l'imprimante de sortie :

Printer.PrinterIndex := Imprimantes.ItemIndex;

Enfin, dans l'événement OnClick du bouton Episode1, nous allons créer le travail d'impression:

var
S: String;
begin
With Printer do
begin
{Démarrage de l'impression}
BeginDoc;
{Tracé de la zone imprimable}
Canvas.Rectangle(0, 0, PageWidth, PageHeight);

{Dans chaque coin de la feuille, nous allons écrire le texte 'Delphi c'est super'}
S := 'Delphi c''est super';
//Augmentons la taille de la fonte pour une meilleure visibilité
Canvas.font.Size := 12;
// Ecrivons en haut à gauche
Canvas.TextOut(0, 0, S);
// en haut à droite. La position X sera donc égale à 0 + Largeur_de_la_page - Largeur_du_texte_à_imprimer
Canvas.TextOut(PageWidth - Canvas.TextWidth(S), 0, S);
// en bas à gauche
Canvas.TextOut(0, PageHeight - Canvas.TextHeight(S), S);
// en bas à droite
Canvas.TextOut(PageWidth - Canvas.TextWidth(S), PageHeight - Canvas.TextHeight(S), S);
// Enfin, au centre de la feuille
{Ici, on doit utiliser Round car les coordonnées X,Y doivent être des integer}
Canvas.TextOut(round((PageWidth - Canvas.TextWidth(S)) / 2),
round((PageHeight - Canvas.TextHeight(S)) / 2),
S);

{Envoi des commandes à l'imprimante}
EndDoc;
end;

Vous n'avez plus qu'à tester! Vous pouvez téléchargé le code de cet exemple ICI.

La prochaine fois, nous parlerons plus en détail des API GetDeviceCaps, DrawText, CreateFont et TabbedTextOut.

II - L'impression avec Delphi (épisode 2)

Ici, nous allons parler des API que nous allons pouvoir utiliser avec Delphi pour compléter notre objet Printer. Intéressons-nous à GetDc, GetDeviceCaps, DrawText, CreateFont et TabbedTextOut.

II-A - GetDc

On retrouve parfois sur les forums delphi les questions suivantes:

« Comment imprimer l'écran sur une feuille A4? »
« Comment imprimer ma grille de données ? »

L'impression d'un composant visuel (TForm, TDBGrid, TImage, etc ..) passe par l'obtention d'un Hdc sur ce composant avec l'API GetDC.

Le code suivant est un bon exemple qui illustre l'effet sans imprimer (placer un bouton sur une form et mettre çà sur son onclick)

procedure TForm1.Button4Click(Sender: TObject);
var
H: integer;
begin
H := GetDc(GetDeskTopWindow);   {Obtention d'un handle de device context sur l'écran}
{Impression sur le canvas imprimante de la représentation graphique du hdc de l'écran}
{On utilise StretchBlt pour redimensionner la source à la taille de la destination }
StretchBlt(Self.Canvas.Handle,
0,0,
self.Width, self.Height,
H,
0,0,
Screen.Width, Screen.Height,
SRCCOPY);
releaseDc(handle, H);  {ne pas oublier de relacher le hdc}
end;

Il faut se souvenir que ceci n'est qu'une copie graphique de la partie visible de l'élément à imprimer. Ainsi, si une fenètre (par exemple) recouvre une partie de l'élément à imprimer, vous imprimerez également la partie de l'autre élément (indésirable dans ce cas). Vous remarquerez que vous pouvez utiliser cette technique pour créer une image de votre écran à un instant T pour la sauvegarder au format bmp par exemple.

S'il s'agit d'imprimer le contenu d'une TForm, il faut rappeler que cet objet possède une fonction GetFormImage qui renvoie un TBitmap, ce dernier pouvant être sauvegardé sous forme de fichier ou imprimé avec les techniques indiquées dans ce tutoriel. A ce propos, vous pouvez consulter l'aide Delphi sur TCustomForm.GetFormImage.

II-B - GetDeviceCaps

Dans l'épisode 1, nous avons vu que l'objet Printer utilise GetDeviceCaps de façon transparente pour l'utilisateur afin de lui fournir divers renseignements sur l'imprimante. On peut ainsi connaître la hauteur et la largeur de la page en pixels. Mais GetDeviceCaps permet aussi d'obtenir d'autres renseignements intéressants. Rappelons que GetDeviceCaps nécessite un handle et une constante pour lui indiquer la valeur que nous voulons obtenir.

Pour illustrer notre propos, reprenons le projet exemple de l'épisode 1. Commencez par ajouter un bouton de commande que nous appelerons (évidemment) Episode2. Placez sur la feuille un composant TMemo que nous appelerons Details. L'écran devrait ressembler à l'image suivante:

Dans l'événement OnClick du bouton Episode 2 nous allons écrire le code suivant:

var
H: integer;
begin
// On place dans une variable le handle de l'imprimante
H := Printer.handle;

//La constante HORZSIZE nous fournit la largeur en millimètres
Details.Lines.Add(format('Largeur zone imprimable en mm: %d',  [GetDeviceCaps(H, HORZSIZE)]));
//La constante VERTSIZE nous fournit la hauteur en millimètres
Details.Lines.Add(format('Hauteur zone imprimable en mm: %d', [GetDeviceCaps(H, VERTSIZE)]));
//La constante HORZRES nous fournit la largeur en pixels
Details.Lines.Add(format('Largeur zone imprimable en mm: %d', [GetDeviceCaps(H, HORZRES)]));
//La constante VERTRES nous fournit la hauteur en pixels
Details.Lines.Add(format('Hauteur zone imprimable en mm: %d', [GetDeviceCaps(H, VERTRES)]));

{ Vous pouvez voir ici qu'il est maintenant facile de calculer le rapport entre  les pixels }
{ et les millimètres sur votre page imprimée 1 pixel horizontal imprimante = }
{ GetDeviceCaps(H, HORZSIZE) / GetDeviceCaps(H, HORZRES) }
}

{La constante PHYSICALOFFSETX nous fournit la taille en pixels de la zone }
{  non imprimable à partir du bord gauche de la feuille}
Details.Lines.Add(format('pixels non imprimables à gauche: %d',  [GetDeviceCaps(H, PHYSICALOFFSETX)]));

{La constante PHYSICALOFFSETY nous fournit la taille en pixels de la zone }
{ imprimable à partir du bord gauche de la feuille}
Details.Lines.Add(format('pixels non imprimables en haut: %d', [GetDeviceCaps(H, PHYSICALOFFSETY)]));

Je vous laisse vous référer à l'aide Delphi pour toutes les constantes utilisables.

II-C - DrawText

Cette API ressemble énormément à la procédure TextRect de l'objet TCanvas. Elle permet d'écrire un texte à l'intérieur du rectangle (TRect) passé en paramètre. Mais DrawText est bien plus puissante. Comme pour GetDeviceCaps, on peut utiliser certaines constantes pour obtenir différents résultats

Ces constantes sont nombreuses et comme précédemment, je vous invite à voir l'aide sur cette API pour en avoir tous les détails. Certaines méritent d'être détaillées.

DT_CALCRECT: Elle permet de déterminer la largeur et la hauteur du rectangle nécessaire pour contenir le texte passé en paramètre. Cela veut dire qu'avec cette constante, DrawText n'écrit pas le texte mais effectue simplement un calcul. Selon le texte, elle va agir différemment. Si, par exemple, le texte contient des retour à la ligne, DrawText calculera la hauteur du rectangle en fonction du nombre de lignes. Si le texte ne contient qu'une seule ligne, DrawText va augmenter la largeur du paramètre TRect. Vous pouvez ainsi connaitre la taille exacte, au pixel près, du rectangle nécessaire pour contenir votre texte. Continuons pour cela notre code écrit précedemment! Nous devons rajouter 2 variables S et R.

var {rajouter dans la déclaration des variables de la fonction}
S: string;
R: TRect;
begin
{notre précédent code ...............}
R.Left := 10;
R.Top  := 10;
S := 'Calcul rectangle avec DrawText';
DrawText(Printer.handle, PChar(S), length(S), R, DT_CALCRECT);
Details.Lines.add(format('Largeur %d / Hauteur %D', [R.Right, R.Bottom]));
//Rajoutons un saut de ligne pour voir la différence
S := 'Calcul rectangle'#13'avec DrawText';
DrawText(Printer.handle, PChar(S), length(S), R, DT_CALCRECT);
Details.Lines.Add(format('Largeur %d / Hauteur %D', [R.Right, R.Bottom]));

Pour placer votre texte par rapport à un point défini, vous pouvez utiliser les constantes

- DT_TOP, DT_VCENTER, DT_BOTTOM: pour l'alignement vertical

- DT_LEFT, DT_CENTER, DT_RIGHT pour l'alignement horizontal

- DT_END_ELLIPSIS permet d'indiquer à l'impression que le texte n'est pas affiché complètement en tronquant la chaîne de caractères et en y ajoutant des points de suspension.

Voici un exemple d'utilisation de ces constantes de positionnement. Cet exemple utilise un TImage comme sortie mais comme vous le savez maintenant, le canvas d'une image est identique à un canvas d'imprimante.

var
H: integer;
R: TRect;
begin
H := image1.Canvas.handle;
R := image1.ClientRect;
DrawText(H, PChar('NORD'), length('NORD'), R, DT_TOP or DT_CENTER or DT_SINGLELINE);
DrawText(H, PChar('CENTRE'), length('CENTRE'), R, DT_VCENTER or DT_CENTER or DT_SINGLELINE);
DrawText(H, PChar('SUD'), length('SUD'), R, DT_BOTTOM or DT_CENTER or DT_SINGLELINE);
DrawText(H, PChar('NORD_OUEST'), length('NORD_OUEST'), R, DT_TOP or DT_LEFT or DT_SINGLELINE);
DrawText(H, PChar('OUEST'), length('OUEST'), R, DT_VCENTER or DT_LEFT or DT_SINGLELINE);
DrawText(H, PChar('SUD_OUEST'), length('SUD_OUEST'), R, DT_BOTTOM or DT_LEFT or DT_SINGLELINE);
DrawText(H, PChar('NORD_EST'), length('NORD_EST'), R, DT_TOP or DT_RIGHT or DT_SINGLELINE);
DrawText(H, PChar('EST'), length('EST'), R, DT_VCENTER or DT_RIGHT or DT_SINGLELINE);
DrawText(H, PChar('SUD_EST'), length('SUD_EST'), R, DT_BOTTOM or DT_RIGHT or DT_SINGLELINE);
end;

Voici ce que cela donne :

Vous commencez maintenant à entrevoir toutes les possibilités d'impression avec cette API ?

II-D - CreateFont (ou CreateFontIndirect)

Pourquoi utiliser CreateFont alors que le canvas de l'objet Printer possède déjà un une propriété Font ? Et bien tout simplement pour modifier l'orientation de votre texte. En effet, cette API permet de créer une fonte avec un certain angle. CreateFont nous oblige à redéfinir une fonte de toutes pièces. Plutôt que de tout refaire, nous allons utiliser les éléments de la fonte en cours du Canvas. Je vais tout simplement reprendre une procédure trouvée sur le net en y ajoutant quelques commentaires. Elle permet d'écrire sur n'importe quel canvas avec l'orientation désirée

procedure AngleTextOut(CV: TCanvas; const sText: String; x, y, angle:integer);
var
LogFont: TLogFont;
SaveFont: TFont;
begin
{Sauvegarde de la fonte en cours du canvas}
SaveFont := TFont.Create;
SaveFont.Assign(CV.Font);
{Récupération des détails de la fonte dans la structure LogFont}
GetObject(SaveFont.Handle, sizeof(TLogFont), @LogFont);
{Modification à notre guise de l'orientation de la la fonte}
with LogFont do
begin
lfEscapement := angle * 10;
lfPitchAndFamily := FIXED_PITCH or FF_DONTCARE;
end; {with}
{ Création de la fonte par CreateFontIndirect en se basant sur la structure }
{ LogFont et assignation à la fonte du canvas}
CV.Font.Handle := CreateFontIndirect(LogFont);
SetBkMode(CV.Handle, TRANSPARENT);
{Ecriture du texte avec l'inclinaison voulue}
CV.TextOut(x, y, sText);
{On redonne au canvas sa fonte sauvegardée pour lui redonner son état initial}
CV.Font.Assign(SaveFont);
SaveFont.Free;
end;

ATTENTION: Cette possibilité de modification de l'inclinaison ne fonctionne qu'avec les fontes True Type

Pour tester, créer un nouveau projet, poser un composant TImage sur la form, et ajouter la procédure ci-dessus dans le code de la feuille. Ensuite ajouter dans l'événement OnClick de l'image le code exemple pour DrawText et ajouter les lignes suivantes:

image1.Canvas.Font.Name := 'Times New Roman';
AngleTextOut(image1.canvas, 'INCLINE', round(image1.width / 4),   image1.Height, 90);

Tester …

II-E - TabbedTextOut

A quoi va bien pouvoir nous servir cette API ? Tout simplement à écrire nos chaînes de caractères sous forme de colonnes (Tabbed pour tabulations et textout pour écriture du texte). Imaginons que vous vouliez imprimer des chaines de caractères sur 4 colonnes. Vous pourrier calculer les positions X de début de chaque colonne, et écrire 4 fois l'instruction TextOut. Mais TabbedTextOut vous permet de le faire en « une seule passe ». Un petit exemple valant mieux qu'un long discours …. :-)

Nous connaissons la largeur de notre page imprimée avec PageWidth. Si nous désirons 4 colonnes, nous allons créer un tableau de 4 valeurs entières qui contiendra les positions gauches de nos colonnes. A nouveau, j'utilise ici une sortie sur un TImage et vous transposerez vous-même sur le canvas de l'imprimante.

var
XCols: array[1..4] of integer;
S: string;
I, y, H: integer;
begin
XCols[1] := 0;
XCols[2] := round(image1.Width / 4);
XCols[3] := round(image1.Width / 2);
XCols[4] := image1.Width - XCols[2];

y := 0;
H := Image1.Canvas.TextHeight('M'); {la plus haute}
for i := 0 to 5 do
begin
S := format('L%dC0'#9'L%dC1'#9'L%dC2'#9'L%dC3', [I, I, I, I]);
TabbedTextOut(image1.Canvas.handle, 0, y, PChar(S), length(S), 4, XCols[1], 0);
Y := Y + H + 2;
end;

Voici la sortie sur l'image.

Vous voyez tout le parti que vous pouvez tirer de cette API?

Et bien voilà! Cet épisode 2 est terminé. A la prochaine fois pour le 3eme épisode dans lequel nous réunirons les 2 premiers pour faire une petite impression d'une grille de données.

III - Impression avec Delphi (épisode 3)

Nous allons poursuivre notre étude sur l'impression avec Delphi en utilisant les divers éléments vus dans les précédents épisodes pour imprimer une grille de données. Pour cela, nous allons réaliser ensemble ce projet exemple. Nous utiliserons les données de la table employee.db de la base DBDemos fournie comme exemple avec Delphi. Nous allons imprimer les données de la table en ajoutant à notre état de sortie un en-tête et un bas de page.

1) Créez un projet et sauvegardez-le à l'endroit qui vous plaira et ajouter l'unité Printers à la clause uses.

2) Sur la « Tform », disposez à votre guise un TDatasource (Datasource1), un TTable (Datas), un TDBGrid (DBGrid1) et un TButton (Imprimer). Dans l'inspecteur d'objet, définissez les propriétés :

- Datasource1.Dataset := Datas

- DBGrid1.DataSource := Datasource1

- Datas.DatabaseName := DBDemos

- Datas.Active := true

Les données doivent maintenant être affichées dans la grille.

Bien! En ce qui concerne le coté visuel, c'est terminé. Vous pouvez fermer la feuille pour vous concentrer sur le code lui-même. Pour cette fois, j'ai préféré écrire le code et vous le copirez en entier.

implementation

{$R *.DFM}

var

Nous avons d'abord besoin de définir quelques variables globales

Entete, Details, PiedPage: TRect; {pour connaître les limites des zones respectives}
YCurr: integer; {La position en cours pour l'écriture d'une ligne}
C: TCanvas; {Notre canvas pour imprimer}
Li: integer; {nombre de lignes imprimées }
Dx, Dy: integer; {Décalage par rapport aux marges}
NPage: word = 0;  {Le numéro de page à incrémenter à chaque nouvelle page}
TotalPages: word = 0;  {Le nombre de pages à imprimer. Pour afficher Page x / TotalPages }

{ TForm1 }

{Ajoutons 2 fonctions de conversion des millimètres en pixels (voir « Travaillez en mm sur les imprimantes ») }

function Millimetres2PixelsX(Millims: integer): integer;
begin
result := MulDiv(GetDeviceCaps(Printer.Handle, LOGPIXELSX), 10 * Millims, 254);
end;

function Millimetres2PixelsY(Millims: integer): integer;
begin
result := MulDiv(GetDeviceCaps(Printer.Handle, LOGPIXELSY), 10 * Millims, 254);
end;

Une procédure pour modifier plus facilement la fonte de notre canvas d'imprimante

procedure DefFonte(F: TFont; Nom: string; Taille: integer; Styl: TFontStyles);
begin
with F do begin
Name := Nom;
Size := Taille;
Style := Styl;
end;
end;

Dans l'en-tète, nous allons imprimer en haut à gauche: La date Centré (horizontal et vertical), le nom de la table en haut à droite, le numéro de la page

procedure TForm1.ImprimeEnTete;
var
S: string;
begin
{On dessine l'entourage (si on le désire)}
C.Rectangle(Entete);

DefFonte(C.Font, 'Arial', 10, []);

{ Ecriture de la date en haut à gauche en n'oubliant pas les }
{ décalages par rapport aux marges }
{ Vous remarquerez que l'on aurait aussi pu utiliser DrawText avec DT_LEFT or DT_VTOP}
C.TextOut(Entete.Left + Dx, Entete.top + Dy, DateToStr(now));
{Notre page en cours}
S := format('Page %d / %d', [NPage, TotalPages]);
C.TextOut(Entete.Right - C.TextWidth(S) - Dx, Entete.top + Dy, S);

{Enfin, centré au milieu de notre rectangle d'en-tète le nom de la table}
DefFonte(C.Font, 'Times New Roman', 24, []);
DrawText(C.handle,  PChar(Datas.TableName),  length(Datas.tablename),
Entete,  DT_VCENTER or DT_SINGLELINE or DT_CENTER);
{On dessine si on le désire un entourage autour de la zone Détails}
C.Rectangle(Details);
end;

Dans le pied de page, nous allons centrer (horizontal et vertical) le nombre d'enregistrements déjà imprimés par rapport au nombre total d'enregistrements

procedure TForm1.ImprimePiedPage;
var
S: string;
begin
C.Rectangle(PiedPage);
DefFonte(C.Font, 'Arial', 10, []);
S := format('%d lignes imprimées sur un total de %d', [Li, Datas.RecordCount]);
DrawText(C.handle,  PChar(S),  length(S),  PiedPage,  DT_VCENTER or DT_SINGLELINE or DT_CENTER);
end;

J'ai découpé le code en procédures distinctes pour mieux structurer le déroulement du programme

J'utilise ici une variable RealNewPage qui va nous indiquer si on doit demander une nouvelle page à Printer. Pourquoi ? Tout simplement parce que cette procédure NouvellePage va être utilisée en tout premier avant même de commencer l'impression des lignes. Donc, au premier passage, la nouvelle page existe déjà (créée par Printer.BeginDoc). Si on ne prenait pas cette précaution, nous aurions une page blanche à chaque impression lancée.

procedure TForm1.NouvellePage(RealNewPage: boolean);
begin
if RealNewPage then Printer.NewPage;
Inc(NPage);
ImprimeEnTete;
YCurr := Details.Top + Dy;
end;

procedure TForm1.ImprimerClick(Sender: TObject);
var
HM: integer;
T: array of integer;
S: string;
Bm: TBookmark;
Bl: boolean;
LignesParPage: word;
begin

Li := 0;  {initialisation du nombre de lignes imprimée}
Dx := Millimetres2PixelsX(2); {Calcul en pixels d'un décalage haut et bas de 2 mm }
{en cas d'impression texte près des marges}
Dy := Millimetres2PixelsY(2);

with Printer do begin
Orientation := poLandScape;
BeginDoc;

{Hauteur de l'en-tète = 10% de la hauteur totale}
Entete := Rect(0, 0, PageWidth, muldiv(PageHeight, 10, 100));
{Hauteur du pied de page = 5% de la hauteur totale}
PiedPage := Rect(0, PageHeight - muldiv(PageHeight, 5, 100), PageWidth, PageHeight);
end;

with Details do
begin
Left := 0;
Right := Printer.PageWidth;
{Haut de la zone détails à 10 mm sous le bas de l'en-tète}
Top := EnTete.bottom + Millimetres2PixelsY(10);
{Bas de la zone détails à 5 mm au-dessus du haut du pied de page}
Bottom := PiedPage.Top - Millimetres2PixelsY(5);
end;

Voici le schéma de notre « découpage »

C := Printer.Canvas;  {Pour un code plus concis}

//Calcul du nombre de ligne par page, et ainsi du nombre de page
DefFonte(C.Font, 'Arial', 11, []);
Hm := C.TextHeight('M');     {On calcule la hauteur d une ligne en pixels}
{On peut ainsi calculer le nombre de lignes que peut contenir la zone Details}
LignesParPage := (Details.Bottom - Details.Top - Dy) div HM;
{Le nombre de pages sera calculé en divisant le nombre total de lignes par LignesParPage}
TotalPages := Datas.RecordCount div LignesParPage;
{ATTENTION: on doit également vérifier si les pages seront complètes.}
{ Avec mod on peut voir s'il nous restera des lignes à imprimer en fin de document}
Bl := (Datas.RecordCount mod LignesParPage) <> 0;
{Si on obtient un reste, il nout faut ajouter une page}
if BL then inc(TotalPages);
{Prenons un exemple! Si la zone Details peut contenir 35 lignes et si nous avons }
{42 lignes à imprimer, nous aurons les valeurs suivantes: }
{TotalPages := 42 div 35; Donc  TotalPages  =  1 (1 page de 35 lignes)
{Bl := (42 mod 35) <> 0 ;  Donc  true puisque 42 mod 35 = 7.
{Ainsi, nous avons défini que nous aurons 1 page complète de 35 lignes et une page }
{incomplète de 7 lignes}
{Utilisons le nombre de colonnes de la DBGrid pour définir nos colonnes sur la sortie imprimée}
{Bien sur, cela n'est valable que si vous avez un nombre raisonnable de colonnes.}
{La table utilisée dans l'exemple s'y prète bien.
{Dans un autre cas, vous serez obligé de définir quelles colonnes vous voulez imprimer}
SetLength(T, DBGrid1.columns.count);
DefiniColonnes(T);

NPage := 0;
{Le paramètre de NouvellePage est false, car la page est déjà créée par BeginDoc}
NouvellePage(false);

with Datas do begin
Bm := GetBookmark;
DisableControls;
first;
while not eof do
begin
{Utilisation d'une procédure pour construire la chaîne à utiliser avec TabbedTextOut}
DefLigne(S);
{à chaque ligne on incrémente le nombre de lignes imprimées (pour le bas de page)}
Inc(Li);
TabbedTextOut(C.handle, Dx, yCurr, PChar(S), length(S), high(T), T[0], 0);
{après impression de la ligne en cours, on passe à la ligne suivante}
next;
{Ici, nous allons gérer la nécessité de changer de page. yCurr est notre position }
{d'impression courante sur notre page.}
{ Puisque nous connaissons la hauteur d'une ligne, nous allons incrémenter yCurr }
{ de cette hauteur pour définir la valeur suivante en y}
yCurr := yCurr + Hm;
{Ici, nous devons vérifier si yCurr est toujours à l'intérieur de la zone Details,}
{ et surtout si la place est suffisante pour imprimer une ligne}
if YCurr > (Details.bottom - Hm) then
begin
ImprimePiedPage;
NouvellePage(true);
end;

end;
{Enfin, si le nombre de lignes ne correspondait pas à des pages complètes,}
{ il faudrait imprimer le bas de page. On est obligé d'imprimer le pied de }
{ page seulement quand une page de détails a été créée pour connaitre le nombre }
{ de lignes déjà imprimées. Dans un autre cas, ImprimerPiedPage pourrait être }
{ ajouté à la procédure NouvellePage}
if Bl then ImprimePiedPage;

GotoBookmark(Bm);
FreeBookmark(Bm);
EnableControls;
end;
Printer.EndDoc;
end;

Pour définir mes colonnes ainsi que leur largeur respective, j'utilise la largeur des colonnes de la DBGrid. Connaissant la largeur de la DBGrid elle-même, je peux calculer le pourcentage de la largeur grille utilisée par chaque colonne Je reporte ce pourcentage sur la largeur de la zone Details Je n'ai pas cherché ici à savoir si mes colonnes remplissent toutes la grilles. Si vous désirez étaler au maximum vos colonnes, utilisez la somme des largeurs des colonnes utilisées plutôt que la largeur de la grille

procedure TForm1.DefiniColonnes(var Tb: array of integer);
var
I, J, L: integer;
T: array of single;
begin
L := Details.Right - Details.Left;
SetLength(T, high(Tb));

with DBGrid1 do  for I := low(T) to high(T) do T[I] := L * (Columns[I].Width / Width);

Tb[0] := Details.Left;
for I := low(Tb) + 1 to high(Tb) do Tb[I] := Tb[I - 1] + round(T[I - 1]);

end;

procedure TForm1.DefLigne(var S: string);
var
I: integer;
begin
S := '';
with Datas do for I := 0 to Fields.Count - 1 do S := S + Fields[I].AsString + #9;
SetLength(S, Length(S) - 1);  {Pour supprimer le dernier caractère de tabulation}
end;

end.

Voilà! Votre impression de grille est terminée. J'ai utilisé TabbedTextOut pour l'exemple. Mais, pour notre grille de données, il aurait été plus présentable d'imprimer chaque valeur séparément. En effet, une présentation correcte de nos données aurait nécessité un formatage à 2 décimales des valeurs numériques ainsi qu'un alignement à droite. Dans ce cas, pour imprimer une copie plus poussée de notre grille, vous pourriez utiliser les propriétés de chaque objet TColumn de DBGrid1.Columns. Mais avec ce que vous avez appris jusqu'à maintenant, ce sera pour vous un jeu d'enfant.

Dans le prochain et dernier épisode (bientôt), nous réaliserons pour notre grille un module d'aperçu avant impression.

IV - L'aperçu avant impression (épisode 4)

Pour terminer cette série sur l'impression avec Delphi, nous allons voir comment réaliser un aperçu avant impression. Il existe beaucoup de composants (gratuits ou pas) qui permettent ce genre de chose. Il en est un que j'affectionne particulièrement et qui s'appelle TMWPrintObject.Il a été développé par James « Woody » Woodard. N'hésitez pas à lui réclamer (en anglais) la dernière version avec l'exemple qui l'accompagne, c'est en freeware et très bien fait. Pensez juste à lui dire que je vous ai parlé de lui, il verra que j'ai tenu parole :-) . Mais là n'est pas notre propos d'aujourd'hui.

Avant toute chose, il nous faut parler des objets TMetafile et TMetafileCanvas. Ce sera la base de notre étude.

TMetafile est un objet Delphi qui facilite la création d'un métafichier. Cet objet graphique pourra être sauvegardé sous forme de fichier et importé dans tout logiciel acceptant le format WMF. Cela nous offrira la possibilité de sauvegarder une (ou plusieurs) page de notre état pour une réutilisation ultérieure.

Le TMetafile ne possédant pas de Canvas pour dessiner, nous devrons utiliser un TMetafileCanvas.

La technique va consister en la création d'autant de métafichiers que de pages à imprimer. Chaque page étant devenue un fichier WMF, il nous sera alors possible de transférer chaque fichier-page soit sur la canvas de l'imprimante, soit sur n'importe quel canvas. Un simple transfert avec BitBlt ou StretchBlt nous permettra une redirection de nos commandes d'impression sur l'objet de notre choix.

Un TMetafile étant un objet comme les autres, nous stockerons les diverses pages dans un TList. Notre TList sera donc notre liste de pages à imprimer.

Schématisons ce raisonnement:

Comment fonctionne TMetafileCanvas ?

La création d'un TMetafileCanvas passe d'abord par la création d'un TMetafile. Ensuite TMetafileCanvas sera utilisé comme n'importe quel TCanvas. L'utilisation de cet objet se fait de la manière suivante:

var
MetaFile: TMetaFile;
MetaFileCanvas: TMetaFileCanvas;
begin
// Création du TMetaFile
MetaFile := TMetaFile.Create;
{ Création du TMetaFileCanvas en utilisant le TMetaFile comme paramètre.}
{ Cela indique à Delphi que tout ce qui sera envoyé à destination du canvas }
{ devra passer dans le TMetaFile.}
MetaFileCanvas := TMetaFileCanvas.Create(MetaFile, 0);
MetaFileCanvas.TextOut(10,10,'Ce qu''on veut');
{ La libération du TMetaFileCanvas entraine le transfert des informations dans le TMetaFile}
MetaFileCanvas.free;
{ Ici, nous pourrions sauvegarder MetaFile et obtenir ainsi notre texte }
{ sous forme de fichier wmf}
end;

Nous allons réunir tous les éléments que nous connaissons afin de créer un PrintPreview simple.

Lors de sa création, notre métafichier pourra prendre la dimension voulue en pixels par ses propriétés Width et Height. Notre aperçu avant impression utilisera un TPaintBox placé sur notre TForm. Il serait également possible d'utiliser un TImage pour profiter de la persistance de l'affichage mais les performances seront meilleures en terme de rapidité et de ressources utilisées. Il faudra donc que le programme redessine la page en cours d'affichage à chaque événement OnPaint du composant. Allons-y !

1) Créez un nouveau projet et nommez la feuille principale Previsualisation.

2) Placez un composant TPaintBox (onglet Système) en haut à gauche de votre form. Définissez ses propriétés comme suit:

Name := 'Prv';
Top := 10;
Left := 10;
Height := 594;
Width := 420; {format A4 = 210 * 297 mm --> 420 * 594 pixels}

3) A côté de PaintBox1, placer 2 boutons de commandes respectivement Avant et Après pour les propriétés name de chacun.

Votre feuille devrait ressembler à ceci :

4) Déclarer dans la section Private une variable de type TList pour contenir nos différentes pages:

LPages: TList;

5) Ajouter le code suivant dans l'événement OnCreate de votre feuille

LPages := TList.Create;

6) Ajouter le code suivant dans l'événement OnDestroy de votre feuille (libération des ressources utilisées)

while LPages.Count > 0 do
begin
Dispose(LPages[0]);
LPages.Delete(0);
end;
LPages.Free;

A présent, nous allons ajouter du code à l'événement OnCreate de notre feuille.

var
I, P: integer;
S: String;
R: TRect;

En avant pour le code ! Nous allons créer 3 pages à ajouter au TList LPages. La première page contiendra du texte simple, la seconde des tracés graphiques et la dernière une image. Tout va se passer dans l'événement OnCreate de notre feuille de prévisualisation.

Pour commencer, nous allons stocker la valeur indiquant le nombre pixels dans un pouce (souvenez-vous que pour l'imprimante, la valeur est la même en x et en y). Cette valeur va nous servir plusieurs fois, et j'ai donc jugé opportun de la stocker dans une variable.

P := GetDeviceCaps(Printer.handle, LOGPIXELSX);

Pour cet exemple, je sais déjà qu'il y aura 3 pages à prévisualiser. Je vais donc ajouter directement mes 3 métafichiers à ma liste LPages. Ici, on ne peut pas utiliser une variable locale, car vous le savez, une variable locale à une procédure a une durée de vie égale à celle de la-dite procédure. Je vais donc utiliser une méthode de création directement sous forme de paramètre à la fonction add de mon TList. Pour chaque métafichier, il faut indiquer une largeur et une hauteur en utilisant les propriétés Width et Height. Comme chaque métafichier est destiné à être imprimé sur un canvas d'imprimante, je vais initialiser la largeur de chaque métafichier avec Printer.PageWidth et la hauteur avec Printer.PageHeight.

for I := 0 to 2 do
begin
LPages.add(TMetafile.Create);
with TMetafile(LPages[I]) do
begin
Width  := Printer.PageWidth;
Height := Printer.PageHeight;
end;
end;

A partir de là, nous allons reprendre les métafichiers 1 par 1.

Pour chacun, nous allons créer un canvas
Sur le premier canvas, écrivons un texte en haut à gauche avec la méthode Canvas.TextOut et un texte centré avec l'API DrawText.

ATTENTION: Ici, une chose très importante. Il est indispensable, pour obtenir un aperçu correct des impressions texte, de modifier la propriété PixelsPerInch du canvas. En effet, lors de la copie sur le canvas du PaintBox, il en sera tenu compte par Delphi. Quand vous aurez terminé l'écriture de ce code, faites l'essai en mettant cette ligne en commentaire et visualisez le résultat.

with TMetafileCanvas.create(TMetafile(LPages[0]), 0) do begin
Font.PixelsPerInch := P;

font.Size := 18;
TextOut(10, 10, 'Vous êtes sur la page 1');
font.Size := 22;
S := 'Ce texte est centré sur la page';
R := Rect(0, 0, Printer.PageWidth, Printer.PageHeight);
DrawText(handle, PChar(S), length(S), R, DT_CENTER or DT_VCENTER or DT_SINGLELINE);
free;
end;

Sur la seconde page, je vais dessiner un rectangle et plusieurs ellipses imbriquées avec des couleurs différentes. L'API InflateRect utilisée permet de réduire ou aggrandir le TRect qui définit l'emplacement de l'ellipse.

with TMetafileCanvas.create(TMetafile(LPages[1]), 0) do begin
inflateRect(R, -300, -300);
Ellipse(R);
Pen.Color := clBlue;
inflateRect(R, -200, -200);
Ellipse(R);
Pen.Color := clGreen;
inflateRect(R, -200, -200);
Ellipse(R);
Pen.Color := clRed;
inflateRect(R, -200, -200);
Ellipse(R);
inflateRect(R, -200, -200);
Rectangle(R);
free;
end;

Pour terminer, nous allons capturer l'écran du PC et l'afficher au centre de la page.

I := GetDc(0);   {Obtention d'un handle de device context sur l'écran}
with TMetafileCanvas.create(TMetafile(LPages[2]), 0) do begin
StretchBlt(Handle,
0,0,
Printer.PageWidth, Printer.PageHeight,
I,
0,0,
Screen.Width, Screen.Height,
SRCCOPY);
free;
end;
releaseDc(handle, I);  {ne pas oublier de relâcher le hdc}

Prv.tag := 0;

Voilà ! Nos 3 pages sont crées. Il s'agit maintenant de gérer l'affichage. Cela va se faire naturellement par l'événement OnPaint du TPaintBox. Pour savoir quel page doit s'afficher, nous allons utiliser la propriété tag du PaintBox. Si tag est égal à 2, le PaintBox affichera l'élément 2 du TList LPages.

procedure TPrevisualisation.PrvPaint(Sender: TObject);
var
M: TMetafile;
begin
if LPages.Count > 0 then
begin
Prv.Canvas.Brush.Color := clWhite;
M := TMetafile(LPages[Prv.tag]);
Prv.Canvas.FillRect(Rect(0,0, M.width, M.height)); {Pour "peindre" le fond en blanc.}
Prv.canvas.StretchDraw(Rect(0, 0, Prv.width, Prv.Height), M);
end;
end;

Il ne reste plus qu'à coder les événements OnClick des boutons Avant et Arriere

procedure TPrevisualisation.AvantClick(Sender: TObject);
begin
if Prv.tag > 0 then
begin
Prv.tag := Prv.tag - 1;
Prv.refresh;
end
else
ShowMessage('Vos êtes sur la première page visible');
end;

procedure TPrevisualisation.ApresClick(Sender: TObject);
begin
if Prv.tag < LPages.Count - 1 then
begin
Prv.tag := Prv.tag + 1;
Prv.refresh;
end
else
ShowMessage('Vos êtes sur la dernière page visible');
end;

Et c'est tout ! Alors ? Compliqué ? Vous voyez que non ! Pour terminer, vous rajouterez la commande d'impression qui sera tout simplement:

procedure TPrevisualisation.lmprimerClick(Sender: TObject);
begin
with Printer, Canvas do
begin
BeginDoc;
Draw(0, 0, TMetafile(LPages[0]));
NewPage;
Draw(0, 0, TMetafile(LPages[1]));
NewPage;
Draw(0, 0, TMetafile(LPages[2]));
EndDoc;
end;
end;

Avec ce dernier chapitre, vous avez maintenant toutes les clés pour imprimer ce que vous voulez, avec une preview personnalisée à votre convenance. Vous pouvez même sauvegarder vos états de sortie sous forme de fichier Wmf pour une réimpression ultérieure facile.

Code Delphi 5

Bonne continuation!