技術記事

DelphiのPDFiumコンポーネントによる並べて表示するPDF比較

2つのドキュメントを同時に開き、同じページ番号で、それぞれが独自のスクロール可能なパネルに表示される:それが比較ビューアーの核心です。PDFiumコンポーネントは、TPdfがファイルを所有し、TPdfViewが表示を所有するという単純なオブジェクトモデルを通じてこれを提供します。1つのドキュメントに1つのTPdf、1つのTPdfViewです。3つのパネルが必要な場合は、3つのペアを用意します。難しいのはAPI呼び出しではありません。ウィンドウのサイズ変更時のレイアウト計算と、どのビューに従ってページを同期するかのロジックが要点です

フォームのレイアウト

VCLフォームには、3つのTScrollBoxコンテナーが並んで保持されており、それぞれの中にTPdfViewがあり、alClientに配置されてボックスを埋めます。ユーザーが実行時に列幅を調整できるように、ボックス間に2つのTSplitterコンポーネントが配置されています。パネルの上のツールバーには、開くボタン、ズームコントロール、および2ビュー/3ビューの切り替えが含まれています

3ビューモードは、フォームが内部的に追跡するブール値です。これが切り替わると、幅を再計算し、3番目の列を表示または非表示にします。最も簡単なアプローチは、すべてのAlignプロパティをクリアし、スプリッターを非表示にしてから、絶対位置を設定することです:

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;

整数演算の前に3つのボックスすべてにAlign := alNoneを設定することで、VCL制約エンジンが割り当てと競合するのを防ぎます。2ビューモードでドラッグしてサイズ変更を行いたい場合は、配置後にスプリッターの可視性を復元します

各スクロールボックスの高さは、クライアント領域からツールバーパネルの高さを引いたものです。ツールバーはalTopで上部にドッキングされているため、ClientHeight - PanelButtons.Heightで使用可能な垂直スペースが得られます。これを同じUpdateLayout呼び出し内で3つのボックスすべてに割り当てて、1つのボックスが他のボックスよりも高くなり、レイアウトのちらつきが発生するフレームが絶対にないようにします

ドキュメントを開く

各パネルのペアには、独自の開くプロシージャが必要です。パターンは短いです:コンポーネントを非アクティブにし、ファイル名を設定し、アクティブにしてから、Activeを確認します;Falseのままであった場合は、パスワードの入力を求めて再試行します。TPdfView.Activeはレンダリングを制御するものですが、TPdf.Activeは実際にファイルを開くものであることに注意してください;これらは独立しています。リンクされたTPdfがまだアクティブでないときにPdfView.Active := Trueを設定しても無害ですが、何も表示されません

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;

割り当て後は常にPdfComponent.Activeを確認してください;ファイルが破損しているか、パスワードが間違っていると、デフォルトのパスでは例外を発生させることなくロードが静かに失敗します。正常に開いた後に明示的にPdfViewComponent.PageNumber := 1を設定することで、前のドキュメントの古いページ番号を回避できます

最後のメッセージダイアログは意図的なものです:破損したファイルやサポートされていないファイルは、静かな空白のパネルとして飲み込まれるのではなく、すぐに表面化させたいからです。何も見えないユーザーは、ファイルが読み込まれて単に空であるのか、それともコンポーネントがそれを拒否したのか見当がつきません。障害を報告することで、エラーが目に見える状態に保たれます

アクティブなパネルの追跡

ユーザーがパネル内をクリックすると、そのパネルがアクティブになります。フォームはプライベートなFActivePdfView: TPdfViewフィールドを追跡します。視覚的フィードバックは、含まれているTScrollBoxの境界線の色の変更です:アクティブなものにはclHighlightを設定し、他のものにはclWindowを設定します。これを各TPdfView.OnClickおよび開くプロシージャに配線して、フォーカスが新しく開いたドキュメントに従うようにします

一部の操作は、アクティブなパネルだけでなく、表示されているすべてのパネルに適用されます。フォーム上のブール値FAllViewsModeがそのブランチを駆動します。それが真である場合、ズームの変更とページナビゲーションは、アクティブなドキュメントを持つすべてのパネルにファンアウトします:

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;

同期されたページナビゲーション

同期されたナビゲーションはオプションですが、両方のファイルが同じページ範囲をカバーするドキュメント改訂ワークフローに役立ちます。ロジックは、ユーザーが1つのビューをナビゲートした後に発生するイベントハンドラーに属します。ソースビューのPageNumberが変更されると、ハンドラーはその番号を他のビューに伝播しますが、1つのガードの対象となります:ターゲットビューには少なくともそれだけのページが必要であり、そうでない場合はスキップします

TPdfViewPageNumberTPdfPageNumberは独立しています。TPdf.PageNumberは、ドキュメントコンポーネントが現在考慮しているページを追跡します;TPdfView.PageNumberは、画面に表示されているものを追跡します。ナビゲーションの目的では、ドキュメントプロパティではなくビュープロパティが必要になります

「ページの同期」のようなラベルの付いたチェックボックスは、ユーザーにコントロールを与えます。オフにすると、各パネルは独立してナビゲートし、ハンドラーは直ちに終了します。この独立性は、2つのドキュメントのページ数が異なるユースケースや、ユーザーが異なるページから始まる翻訳で同等のパッセージを見つけたいユースケースにとって重要です。常に同期を強制すると、単純な2ウィンドウのデスクトップ配置よりもツールの使用が難しくなります

注意すべき点:同期ハンドラー内でプログラムによってPdfView.PageNumberを設定すると、そのビューで変更イベントがトリガーされます。割り当て前に設定し、割り当て直後にクリアするブール値フラグを使用して、無限再帰からガードしてください。フラグは、3つのビューすべてが同じハンドラーを共有するため、ビューごとではなくフォームごとになります

パネルごとのズーム

TPdfViewには独自のZoomプロパティがあり、Zoom := 100が実際のサイズ(100%)を意味するパーセントのDoubleです。これを設定すると、アクティブなFitModeが上書きされます。アクティブパネルの幅に合わせるボタンについては、PdfView.PageWidthZoom[PdfView.PageNumber]からフィットズームを読み取って割り当てます。ページに合わせるには、PageZoom[PageNumber]を使用します。どちらも1ベースのページ番号でインデックス付けされた配列プロパティであるため、アクセスする前にページ番号がゼロにならないようにガードしてください

現在のページを画像にエクスポートするときは、ビューから回転を読み取りますが、ビューではなくTPdfコンポーネントでRenderPageを呼び出します。TPdf.RenderPageのビットマップ形式は、明示的なピクセル寸法と、TRotation値およびTRenderOptionsセットを受け取ります。関数のバリアントは、呼び出し元が所有するTBitmapを返し、保存後に自分で解放します:

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;

幅と高さに対する2倍の乗数は、細かいテキストを含むドキュメントの出力をより鮮明にします。ビットマップを解放する部分のtry/finallyはオプションではありません;TSaveDialogのキャンセルでもfinallyブロックにヒットするため、ユーザーが何をしたかに関係なくビットマップが解放されるようにする必要があります

DLLの要件

PDFiumコンポーネントは、ネイティブのpdfiumライブラリをラップします。32ビットのホストプロセスにはpdfium32.dllが必要です;64ビットのホストにはpdfium64.dllが必要です。V8 JavaScriptエンジンを搭載したバリアントはv8サフィックスが追加され、5〜6 MBの標準ビルドに対して約23〜27 MBの重さになります。フォーム入力を無効にする(Pdf.FormFill := False)比較ビューアーの場合、標準の非V8ビルドで十分であり、配布サイズを小さく抑えることができます

DLLを実行ファイルと同じディレクトリ、またはシステムPATH上の任意のディレクトリに配置します。最初のTPdfがアクティブ化されたときにコンポーネントがオンデマンドでそれをロードするため、見つからないDLLはアプリケーションの起動時ではなくその時点で表面化します。インストーラーを出荷する場合、最も信頼できるアプローチは、管理者が後でクリーンアップする可能性のあるシステムディレクトリに依存するのではなく、インストール中にDLLをアプリケーションフォルダーにコピーすることです

V8ビルドは主に、計算フィールドをトリガーしたりハンドラーを送信したりするなど、PDFのJavaScriptアクションを操作する必要がある場合に役立ちます。受動的な比較ビューアーにはJavaScriptを実行する理由はありません;Active := Trueの前にPdf.FormFill := Falseを設定すると、フォーム入力環境が完全にスキップされ、標準ビルドが使用されていてもJSエンジンが初期化されないことになります。これは、出荷するDLLバリアントに関係なく、読み取り専用ビューアーの正しいデフォルトです

PDFiumコンポーネントとその完全なAPIの詳細については、Delphi PDFiumコンポーネント製品ページにアクセスしてください