한 번에 두 개의 문서를 엽니다. 동일한 페이지 번호를 가리키며 각각 스크롤 가능한 고유 패널에 표시됩니다. 이것이 비교 뷰어의 핵심입니다. PDFium 컴포넌트는 TPdf가 파일을 소유하고 TPdfView가 표시를 소유하는 단순한 객체 모델을 통해 이를 구현합니다. 한 문서, 한 TPdf, 한 TPdfView. 세 개의 패널을 원한다면 세 쌍을 가지면 됩니다. 어려운 부분은 API 호출이 아닙니다; 창 크기가 조정될 때의 레이아웃 산술과, 어느 뷰가 다른 뷰를 따라갈지 결정하는 페이지 동기화 로직입니다
폼(Form) 레이아웃
VCL 폼은 내부에 TPdfView가 alClient로 정렬되어 박스를 채우는 세 개의 TScrollBox 컨테이너를 나란히 둡니다. 사용자가 런타임에 열 너비를 조정할 수 있도록 박스 사이에 두 개의 TSplitter 컴포넌트가 위치합니다. 패널 위쪽의 툴바에는 열기 버튼, 줌 컨트롤, 그리고 두 개 뷰 / 세 개 뷰 토글이 있습니다
세 개 뷰 모드는 폼이 내부적으로 추적하는 boolean 값입니다. 이 값이 전환되면 너비를 다시 계산하고 세 번째 열을 표시하거나 숨깁니다. 가장 간단한 방법은 모든 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;
// 세 개의 모든 Height 값에 동일하게 (ClientHeight - 툴바 높이)를 적용
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 호출 내에서 이 값을 세 개의 박스 모두에 할당하여, 하나의 박스가 다른 박스보다 더 길어져서 레이아웃이 깜박이는 프레임이 발생하지 않도록 하세요
문서 열기
각 패널 쌍은 고유한 열기 프로시저가 필요합니다. 패턴은 간단합니다: 컴포넌트를 비활성화하고, 파일 이름을 설정하고, 활성화한 다음 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;
// 불러오기 실패는 조용히 일어납니다: 예외를 발생시키지 않고 Active가 False로 유지됩니다.
if not PdfComponent.Active then
begin
// 암호로 보호된 파일일 가능성이 큽니다; 사용자에게 한 번의 재시도 기회를 줍니다.
if InputQuery('Password', '문서 비밀번호를 입력하세요:', Password) then
begin
PdfComponent.Password := Password;
PdfComponent.Active := True;
end;
end;
if not PdfComponent.Active then
begin
ShowMessage('다음을 열 수 없습니다: ' + OpenDialog.FileName +
' (손상된 파일이거나 잘못된 비밀번호)');
Exit;
end;
PdfViewComponent.PageNumber := 1;
SetActivePdfView(PdfViewComponent);
end;
할당 후에는 항상 PdfComponent.Active를 확인하세요. 손상된 파일이나 잘못된 비밀번호는 기본 경로에서 예외를 발생시키지 않고 조용히 로드에 실패하게 만듭니다. 성공적으로 열린 후 PdfViewComponent.PageNumber := 1을 명시적으로 설정하면 이전 문서의 오래된 페이지 번호를 피할 수 있습니다
마지막의 메시지 대화 상자는 의도적인 것입니다: 손상되거나 지원되지 않는 파일은 조용히 빈 패널로 삼켜지기보다는 즉시 나타나기를 원합니다. 아무것도 표시되지 않으면 사용자는 파일이 로드되었는데 단순히 비어 있는 것인지, 아니면 컴포넌트가 거부한 것인지 알 길이 없습니다. 실패를 보고하여 오류를 눈에 띄게 유지하십시오
활성 패널 추적
사용자가 패널 내부를 클릭하면 해당 패널이 활성화됩니다. 폼은 FActivePdfView: TPdfView 프라이빗 필드를 추적합니다. 시각적 피드백은 포함하는 TScrollBox의 테두리 색상 변경입니다: 활성화된 패널은 clHighlight로 설정하고 다른 패널은 clWindow로 설정합니다. 이를 각 TPdfView.OnClick 및 열기 프로시저에 연결하여 포커스가 방금 연 문서를 따라가도록 합니다
일부 작업은 활성화된 패널뿐만 아니라 눈에 보이는 모든 패널에 적용됩니다. 폼의 boolean FAllViewsMode가 해당 분기를 추진합니다. 이 값이 true일 때 확대/축소 변경 및 페이지 탐색은 활성 문서가 있는 모든 패널로 퍼져 나갑니다:
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;
동기화된 페이지 탐색
동기화된 탐색은 선택 사항이지만 두 파일이 동일한 페이지 범위를 다루는 문서 수정 워크플로에 유용합니다. 로직은 사용자가 하나의 뷰를 탐색한 후 발생하는 이벤트 핸들러에 속합니다. 소스 뷰가 PageNumber를 변경하면 핸들러는 대상 뷰에 최소한 그만큼의 페이지가 있어야 한다는 한 가지 제한 사항에 따라 해당 번호를 다른 뷰에 전파합니다. 그렇지 않으면 건너뜁니다
TPdfView의 PageNumber와 TPdf의 PageNumber는 독립적입니다. TPdf.PageNumber는 문서 컴포넌트가 현재 페이지로 간주하는 페이지를 추적합니다; TPdfView.PageNumber는 화면에 표시되는 내용을 추적합니다. 탐색 목적을 위해서는 문서 속성이 아니라 뷰 속성을 원합니다
"페이지 동기화"와 같은 레이블이 지정된 체크박스는 사용자에게 제어권을 부여합니다. 체크가 해제되면 각 패널이 개별적으로 탐색하고 핸들러는 즉시 종료됩니다. 두 문서의 페이지 수가 다르거나 사용자가 다른 페이지에서 시작하는 번역본에서 동등한 구절을 찾으려는 사용 사례에서 그 독립성은 중요합니다. 항상 동기화를 강제하면 단순한 두 창 데스크탑 배열보다 도구를 사용하기 어렵게 만듭니다
주의할 점 한 가지: 동기화 핸들러 내부에서 프로그래밍 방식으로 PdfView.PageNumber를 설정하면 해당 뷰에 대한 변경 이벤트 자체가 트리거됩니다. 할당 전에 설정하고 직후에 지우는 boolean 플래그로 무한 재귀를 방지하십시오. 세 뷰 모두 동일한 핸들러를 공유하므로 플래그는 뷰 단위가 아니라 폼 단위입니다
패널별 확대/축소
각 TPdfView는 고유한 Zoom 속성을 가지고 있으며, Zoom := 100이 실제 크기(100%)를 의미하는 Double 퍼센트 값입니다. 이 값을 설정하면 활성 상태인 FitMode가 재정의됩니다. 활성 패널의 너비 맞춤 버튼의 경우, PdfView.PageWidthZoom[PdfView.PageNumber]에서 맞춤 확대 비율을 읽어와서 할당합니다. 페이지 맞춤의 경우, PageZoom[PageNumber]를 사용합니다. 둘 다 1부터 시작하는 페이지 번호로 인덱싱된 배열 속성이므로, 접근하기 전에 페이지 번호가 0이 아닌지 확인하세요
현재 페이지를 이미지로 내보낼 때, 회전 정보는 뷰에서 읽어오지만, RenderPage 호출은 뷰가 아닌 TPdf 컴포넌트에 해야 합니다. 비트맵 형태의 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)를 비활성화하는 비교 뷰어의 경우, 표준 non-V8 빌드로도 충분하며 배포 크기를 줄일 수 있습니다
DLL을 실행 파일과 같은 디렉터리나 시스템 PATH에 위치시키십시오. 컴포넌트는 첫 번째 TPdf가 활성화될 때 이를 온디맨드로 로드하므로, DLL이 누락된 문제는 애플리케이션 시작 시가 아니라 그 시점에 나타납니다. 인스톨러를 함께 배포하는 경우, 관리자가 나중에 정리할 수 있는 시스템 디렉터리에 의존하는 것보다 설치 중 애플리케이션 폴더에 DLL을 복사하는 것이 가장 신뢰성 있는 접근 방식입니다
V8 빌드는 주로 PDF JavaScript 작업과 상호 작용할 필요가 있을 때 유용합니다. 예를 들어 계산 필드나 전송 핸들러를 트리거할 때입니다. 수동적인 비교 뷰어는 JavaScript를 실행할 이유가 없습니다; Active := True 이전에 Pdf.FormFill := False로 설정하면 폼 채우기 환경을 완전히 건너뛰며, 이는 표준 빌드를 사용하더라도 JS 엔진이 초기화되지 않음을 의미합니다. 어떤 DLL 변형을 배포하든 읽기 전용 뷰어에는 이것이 올바른 기본값입니다
PDFium Component와 그 전체 API에 대한 더 자세한 내용은 Delphi PDFium Component 제품 페이지를 참조하십시오