Deux documents ouverts en même temps, au même numéro de page, chacun dans son propre panneau défilant : voilà le cœur d'un visualiseur de comparaison. PDFium Component fournit cela par un modèle objet direct où TPdf possède le fichier et TPdfView possède l'affichage. Un document, un TPdf, un TPdfView. Si vous voulez trois panneaux, vous avez trois paires. Le difficile n'est pas dans les appels d'API ; c'est dans l'arithmétique de mise en page au redimensionnement de la fenêtre et dans la logique de synchronisation des pages quand vous décidez quelle vue doit suivre laquelle
Disposition de la fiche
La fiche VCL contient trois conteneurs TScrollBox côte à côte, chacun avec un TPdfView à l'intérieur, aligné en alClient pour qu'il remplisse la boîte. Deux composants TSplitter se placent entre les boîtes afin que l'utilisateur puisse ajuster la largeur des colonnes à l'exécution. Une barre d'outils au-dessus des panneaux porte les boutons d'ouverture, les commandes de zoom et le basculement entre deux et trois vues
Le mode trois vues est un booléen que la fiche suit en interne. Quand il bascule, vous recalculez les largeurs et affichez ou masquez la troisième colonne. L'approche la plus simple consiste à effacer toutes les propriétés Align, masquer les séparateurs, puis fixer des positions absolues :
procedure TFormMain.UpdateLayout;
var
TotalWidth: Integer;
begin
TotalWidth := ClientWidth;
if ThreeViewMode then
begin
ScrollBox3.Visible := True;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 3;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth div 3;
ScrollBox3.Left := ScrollBox2.Left + ScrollBox2.Width;
ScrollBox3.Width := TotalWidth - ScrollBox3.Left;
// Appliquer la même valeur (ClientHeight - hauteur de la barre) aux trois Height
end
else
begin
ScrollBox3.Visible := False;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 2;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth - ScrollBox2.Left;
end;
end;
Mettre Align := alNone sur les trois boîtes avant l'arithmétique entière évite que le moteur de contraintes de la VCL ne combatte vos affectations. Rétablissez la visibilité des séparateurs après le positionnement si vous voulez le redimensionnement par glisser en mode deux vues
La hauteur de chaque boîte de défilement est la zone cliente moins la hauteur du panneau de barre d'outils. Comme la barre d'outils est ancrée en haut avec alTop, ClientHeight - PanelButtons.Height vous donne l'espace vertical utilisable. Affectez cette valeur aux trois boîtes dans le même appel à UpdateLayout, pour qu'il n'existe jamais une trame où une boîte est plus haute que les autres et provoque un scintillement de mise en page
Ouverture d'un document
Chaque paire de panneaux a besoin de sa propre procédure d'ouverture. Le motif est court : désactiver le composant, définir le nom de fichier, activer, puis vérifier Active ; si la propriété est restée à False, demander un mot de passe et réessayer. Notez que TPdfView.Active est ce qui commande le rendu, alors que TPdf.Active est ce qui ouvre réellement le fichier ; les deux sont indépendantes. Mettre PdfView.Active := True alors que son TPdf lié n'est pas encore actif est sans danger mais n'affiche rien
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
PdfViewComponent: TPdfView);
var
Password: string;
begin
if not OpenDialog.Execute then
Exit;
PdfComponent.Active := False;
PdfComponent.FileName := OpenDialog.FileName;
PdfComponent.Password := '';
PdfComponent.Active := True;
// Les échecs de chargement sont silencieux : Active reste False au lieu de lever une exception.
if not PdfComponent.Active then
begin
// Très probablement un fichier protégé par mot de passe ; laissez une seule nouvelle tentative.
if InputQuery('Password', 'Enter document password:', Password) then
begin
PdfComponent.Password := Password;
PdfComponent.Active := True;
end;
end;
if not PdfComponent.Active then
begin
ShowMessage('Could not open ' + OpenDialog.FileName +
' (damaged file or wrong password)');
Exit;
end;
PdfViewComponent.PageNumber := 1;
SetActivePdfView(PdfViewComponent);
end;
Vérifiez toujours PdfComponent.Active après l'affectation ; un fichier endommagé ou un mot de passe erroné fait échouer le chargement en silence, sans lever d'exception dans le chemin par défaut. Définir explicitement PdfViewComponent.PageNumber := 1 après une ouverture réussie évite de garder un numéro de page périmé venu du document précédent
La boîte de message à la fin est intentionnelle : vous voulez que les fichiers corrompus ou non pris en charge apparaissent immédiatement plutôt que d'être avalés sous la forme d'un panneau vide et muet. Un utilisateur qui ne voit rien ne sait pas si le fichier a été chargé et se trouve simplement vide, ou si le composant l'a rejeté. Signaler l'échec garde l'erreur visible
Suivi du panneau actif
Quand l'utilisateur clique dans un panneau, ce panneau devient actif. La fiche suit un champ privé FActivePdfView: TPdfView. Le retour visuel est un changement de couleur de bordure sur le TScrollBox conteneur : mettez-la à clHighlight pour le panneau actif et à clWindow pour les autres. Reliez cela à chaque TPdfView.OnClick ainsi qu'à la procédure d'ouverture, pour que le focus suive le document que vous venez d'ouvrir
Certaines opérations s'appliquent à tous les panneaux visibles plutôt qu'au seul panneau actif. Un booléen FAllViewsMode sur la fiche pilote cette branche. Quand il vaut true, les changements de zoom et la navigation de page se diffusent à tous les panneaux qui ont un document actif :
procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
if PdfView1.Active then PdfView1.Zoom := NewZoom;
if PdfView2.Active then PdfView2.Zoom := NewZoom;
if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;
Navigation de page synchronisée
La navigation synchronisée est facultative mais utile pour les flux de révision de documents où les deux fichiers couvrent la même plage de pages. La logique se place dans un gestionnaire d'événement déclenché après que l'utilisateur a navigué dans une vue. Quand une vue source change son PageNumber, le gestionnaire propage ce numéro aux autres vues, sous une seule réserve : la vue cible doit compter au moins autant de pages, sinon on passe
Les propriétés PageNumber de TPdfView et de TPdf sont indépendantes. TPdf.PageNumber indique la page que le composant document considère comme courante ; TPdfView.PageNumber indique ce qui est affiché à l'écran. Pour la navigation, c'est la propriété de la vue que vous voulez, pas celle du document
Une case à cocher intitulée par exemple « Synchroniser les pages » donne le contrôle à l'utilisateur. Quand elle est décochée, chaque panneau navigue indépendamment et le gestionnaire sort immédiatement. Cette indépendance compte pour les cas où les deux documents ont un nombre de pages différent, ou lorsque l'utilisateur veut retrouver le passage équivalent dans une traduction qui commence à une autre page. Imposer toujours la synchronisation rendrait l'outil plus pénible à utiliser qu'un simple agencement de deux fenêtres sur le bureau
Un point de vigilance : définir PdfView.PageNumber par programme à l'intérieur du gestionnaire de synchronisation déclenchera lui-même l'événement de changement sur cette vue. Protégez-vous de la récursion infinie avec un indicateur booléen que vous positionnez avant l'affectation et effacez immédiatement après. Cet indicateur appartient à la fiche, pas à chaque vue, car les trois vues partagent le même gestionnaire
Zoom par panneau
Chaque TPdfView porte sa propre propriété Zoom, un Double exprimé en pourcentage où Zoom := 100 signifie la taille réelle (100 %). La définir annule tout FitMode actif. Pour un bouton d'ajustement à la largeur sur le panneau actif, lisez le zoom d'ajustement dans PdfView.PageWidthZoom[PdfView.PageNumber] et affectez-le. Pour l'ajustement à la page, utilisez PageZoom[PageNumber]. Les deux sont des propriétés tableau indexées par un numéro de page à base 1, alors prémunissez-vous contre un numéro de page nul avant d'y accéder
Quand vous exportez la page courante en image, lisez la rotation depuis la vue mais appelez RenderPage sur le composant TPdf, pas sur la vue. La forme bitmap de TPdf.RenderPage prend des dimensions explicites en pixels plus une valeur TRotation et un ensemble TRenderOptions. La variante fonction renvoie un TBitmap possédé par l'appelant, que vous libérez vous-même après l'enregistrement :
procedure TFormMain.SaveActiveViewAsImage;
var
Pdf: TPdf;
Bmp: TBitmap;
Jpeg: TJpegImage;
begin
if not Assigned(FActivePdfView) or not FActivePdfView.Active then
Exit;
Pdf := FActivePdfView.Pdf;
Pdf.PageNumber := FActivePdfView.PageNumber;
Bmp := Pdf.RenderPage(
0, 0,
Round(Pdf.PageWidth * 2),
Round(Pdf.PageHeight * 2),
FActivePdfView.Rotation, [], clWhite);
try
if SavePictureDialog.Execute then
begin
Jpeg := TJpegImage.Create;
try
Jpeg.Assign(Bmp);
Jpeg.CompressionQuality := 90;
Jpeg.SaveToFile(SavePictureDialog.FileName);
finally
Jpeg.Free;
end;
end;
finally
Bmp.Free;
end;
end;
Le multiplicateur 2x sur la largeur et la hauteur donne une sortie plus nette pour les documents à texte fin. Le try/finally autour de la libération du bitmap n'est pas facultatif ; une annulation de TSaveDialog passe quand même par le bloc finally, et vous voulez que le bitmap soit libéré quoi qu'ait fait l'utilisateur
Exigences de DLL
PDFium Component encapsule la bibliothèque native pdfium. Un processus hôte 32 bits a besoin de pdfium32.dll ; un hôte 64 bits a besoin de pdfium64.dll. Les variantes avec le moteur JavaScript V8 ajoutent le suffixe v8 et pèsent environ 23 à 27 Mo contre 5 à 6 Mo pour les versions standard. Pour un visualiseur de comparaison qui désactive le remplissage de formulaires (Pdf.FormFill := False), la version standard sans V8 suffit et garde la distribution plus légère
Placez la DLL dans le même répertoire que l'exécutable, ou dans n'importe quel répertoire du PATH système. Le composant la charge à la demande lors de l'activation du premier TPdf, si bien qu'une DLL manquante se manifeste à ce moment-là plutôt qu'au démarrage de l'application. Si vous livrez un installeur, l'approche la plus fiable consiste à copier la DLL dans le dossier de l'application pendant l'installation plutôt que de compter sur un répertoire système qu'un administrateur pourrait nettoyer plus tard
Les versions V8 servent surtout quand vous devez interagir avec les actions JavaScript des PDF, par exemple pour déclencher des champs de calcul ou des gestionnaires de soumission. Un visualiseur de comparaison passif n'a aucune raison d'exécuter du JavaScript ; définir Pdf.FormFill := False avant Active := True écarte entièrement l'environnement de remplissage de formulaires, ce qui signifie aussi qu'aucun moteur JS n'est initialisé même si la version standard est utilisée. C'est la valeur par défaut correcte pour un visualiseur en lecture seule, quelle que soit la variante de DLL que vous livrez
Pour plus de détails sur PDFium Component et son API complète, consultez la page produit Delphi PDFium Component