Teknisk artikel

Sida-vid-sida PDF-jämförelse i Delphi med PDFium Component

Två dokument öppna samtidigt, samma sidnummer, var och en i sin egen skrollbara panel: det är kärnan i en jämförelsevisare. PDFium Component levererar detta genom en okomplicerad objektmodell där TPdf äger filen och TPdfView äger visningen. Ett dokument, en TPdf, en TPdfView. Vill du ha tre paneler har du tre par. De svåra delarna är inte API-anropen; det är layoutaritmetiken när fönstret ändrar storlek och sidsynk-logiken när du bestämmer vilken vy som ska följa vilken

Formulärlayout

VCL-formuläret rymmer tre TScrollBox-behållare sida vid sida, var och en med en TPdfView inuti och justerad med alClient så att den fyller lådan. Två TSplitter-komponenter sitter mellan lådorna så att användaren kan justera kolumnbredder under körning. Ett verktygsfält ovanför panelerna bär öppna-knapparna, zoomkontrollerna och växlingen mellan tvåvys- och trevysläge

Trevysläge är en boolesk variabel som formuläret spårar internt. När den växlar, räknar du om bredder och visar eller döljer den tredje kolumnen. Det enklaste tillvägagångssättet är att rensa alla Align-egenskaper, dölja splitters och sedan ställa in absoluta positioner:

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;
    // Apply the same (ClientHeight - toolbar height) to all three Height values
  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;

Att ställa in Align := alNone på alla tre lådor före heltalsaritmetiken undviker att VCL:s begränsningsmotor kämpar mot dina tilldelningar. Återställ synligheten för splitters efter positionering om du vill ha dra-för-att-ändra-storlek i tvåvysläget

Höjden på varje rullningslåda är klientområdet minus höjden på verktygsfältspanelen. Eftersom verktygsfältet är dockat överst med alTop, ger ClientHeight - PanelButtons.Height dig det användbara vertikala utrymmet. Tilldela detta till alla tre lådor inuti samma UpdateLayout-anrop så det aldrig finns en bildruta där en låda är högre än de andra och orsakar ett layoutflimmer

Öppna ett dokument

Varje panelpar behöver sin egen öppna-procedur. Mönstret är kort: avaktivera komponenten, ställ in filnamnet, aktivera, och kontrollera sedan Active; om det förblev False, fråga efter ett lösenord och försök igen. Observera att TPdfView.Active är det som styr renderingen, men TPdf.Active är det som faktiskt öppnar filen; de är oberoende. Att ställa in PdfView.Active := True när dess länkade TPdf ännu inte är aktiv är ofarligt men visar ingenting

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;

  // Load failures are silent: Active stays False instead of raising.
  if not PdfComponent.Active then
  begin
    // Most likely a password-protected file; give the user one retry.
    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;

Kontrollera alltid PdfComponent.Active efter tilldelningen; en skadad fil eller fel lösenord orsakar att laddningen misslyckas tyst utan att kasta ett undantag i standardvägen. Att explicit ställa in PdfViewComponent.PageNumber := 1 efter en lyckad öppning undviker ett inaktuellt sidnummer från det föregående dokumentet

Meddelandedialogrutan på slutet är avsiktlig: du vill att korrupta eller ej stödda filer ska synas direkt snarare än att sväljas som en tyst tom panel. En användare som inte ser någonting har ingen aning om huruvida filen laddades och helt enkelt är tom, eller om komponenten avvisade den. Att rapportera misslyckandet håller felet synligt

Spårning av aktiv panel

När användaren klickar inuti en panel, blir den panelen aktiv. Formuläret spårar ett privat FActivePdfView: TPdfView-fält. Visuell feedback är en färgändring på kanten av den innehållande TScrollBox: ställ in den till clHighlight för den aktiva och clWindow för de andra. Koppla detta till varje TPdfView.OnClick och till öppna-proceduren så att fokus följer dokumentet du just öppnade

Vissa åtgärder gäller för alla synliga paneler i stället för bara den aktiva. En boolesk FAllViewsMode på formuläret driver den grenen. När den är sann, sprids zoomändringar och sidnavigering ut till varje panel som har ett aktivt dokument:

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;

Synkroniserad sidnavigering

Synkroniserad navigering är valfri men användbar för arbetsflöden med dokumentrevisioner där båda filerna täcker samma sidintervall. Logiken hör hemma i en händelsehanterare som utlöses efter att användaren navigerar i en vy. När en källvy ändrar sitt PageNumber, sprider hanteraren det numret till de andra vyerna, med ett förbehåll: målvyn måste ha åtminstone så många sidor, annars hoppar den över

PageNumberTPdfView och på TPdf är oberoende. TPdf.PageNumber spårar vilken sida dokumentkomponenten anser vara aktuell; TPdfView.PageNumber spårar vad som visas på skärmen. För navigeringsändamål vill du ha vyegenskapen, inte dokumentegenskapen

En kryssruta märkt något i stil med "Synka sidor" ger användaren kontroll. När den är avmarkerad, navigerar varje panel oberoende och hanteraren avslutas omedelbart. Det oberoendet är viktigt för användningsfall där de två dokumenten har olika sidantal, eller där användaren vill hitta motsvarande passage i en översättning som börjar på en annan sida. Att alltid tvinga fram synkning skulle göra verktyget svårare att använda än ett enkelt skrivbordsarrangemang med två fönster

En sak att se upp med: att ställa in PdfView.PageNumber programmatiskt inuti synkroniseringshanteraren kommer i sig att utlösa ändringshändelsen för den vyn. Skydda mot oändlig rekursion med en boolesk flagga som du sätter före tilldelningen och rensar omedelbart efter. Flaggan är per-formulär, inte per-vy, eftersom alla tre vyerna delar samma hanterare

Zoom per panel

Varje TPdfView bär på sin egen Zoom-egenskap, en Double i procent där Zoom := 100 betyder verklig storlek (100%). Att ställa in den åsidosätter eventuellt aktivt FitMode. För en anpassa-till-bredd-knapp på den aktiva panelen, läs in anpassningszoomen från PdfView.PageWidthZoom[PdfView.PageNumber] och tilldela den. För anpassa-till-sida, använd PageZoom[PageNumber]. Båda är array-egenskaper indexerade efter 1-baserat sidnummer, så kontrollera mot ett noll-sidnummer innan du får åtkomst till dem

När du exporterar den aktuella sidan till en bild, läs av rotationen från vyn men anropa RenderPageTPdf-komponenten, inte vyn. Bitmap-formen av TPdf.RenderPage tar explicita pixeldimensioner plus ett TRotation-värde och ett TRenderOptions-set. Funktionsvarianten returnerar en anroparägd TBitmap som du själv frigör efter att ha sparat:

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;

2x-multiplikatorn på bredd och höjd ger skarpare utdata för dokument med fin text. try/finally runt bitmap-frigöringen är inte valfri; ett avbrott i TSaveDialog träffar fortfarande finally-blocket, och du vill ha bitmappen frigjord oavsett vad användaren gjorde

DLL-krav

PDFium Component kapslar in det inbyggda pdfium-biblioteket. En 32-bitars värdprocess behöver pdfium32.dll; en 64-bitars värd behöver pdfium64.dll. Varianter med V8 JavaScript-motorn lägger till v8-ändelsen och väger in på ungefär 23-27 MB jämfört med 5-6 MB för standardbyggen. För en jämförelsevisare som inaktiverar formulärifyllnad (Pdf.FormFill := False), räcker det vanliga icke-V8-bygget och håller distributionen mindre

Placera DLL:en i samma katalog som den körbara filen, eller i vilken katalog som helst på systemets PATH. Komponenten laddar den på begäran när den första TPdf aktiveras, så en saknad DLL dyker upp vid den punkten snarare än vid applikationsstart. Om du levererar ett installationsprogram är det mest tillförlitliga tillvägagångssättet att kopiera DLL:en till applikationsmappen under installationen snarare än att förlita sig på en systemkatalog som en administratör senare kan städa upp

V8-byggena är främst användbara när du behöver interagera med PDF JavaScript-åtgärder, till exempel för att utlösa beräkningsfält eller skicka hanterare. En passiv jämförelsevisare har ingen anledning att köra JavaScript; att sätta Pdf.FormFill := False före Active := True hoppar över formulärifyllnadsmiljön helt, vilket också betyder att ingen JS-motor initieras även om standardbygget används. Det är rätt standard för en skrivskyddad visare oavsett vilken DLL-variant du levererar

För ytterligare detaljer om PDFium Component och dess fulla API, besök Delphi PDFium Component-produktsidan