데이터세트는 행과 열입니다. PDF 페이지는 어느 것에 대한 개념도 없는 빈 좌표 격자입니다. 그 간격을 메우는 것이 여기서 할 일입니다. HotPDF에는 데이터세트를 가져와 형식이 지정된 격자를 넘겨주는 DrawTable 호출이 없습니다. 대신 얻을 수 있는 것은 격자를 만드는 기본 요소입니다. 점에 문자열을 배치하는 TextOut, 글꼴을 선택하는 SetFont, 대역을 칠하는 Rectangle 및 Fill, 선을 그리는 MoveTo / LineTo / Stroke입니다. 작동하는 테이블 내보내기 도구는 행과 열의 생각을 명시적인 x 및 y 좌표로 변환한 다음 데이터가 페이지 하단을 지날 때 해당 좌표를 정직하게 유지하는 규율입니다
다음 예제는 고객 레코드를 보고하지만 그리기 코드의 어떤 부분도 행이 어디에서 오는지 알거나 신경 쓰지 않습니다. 원본은 레거시 TTable을 사용했습니다. FireDAC 쿼리, 메모리 내 데이터세트 또는 레코드의 일반 배열은 변경되지 않은 동일한 루틴에 데이터를 제공합니다. 중요한 것은 한 번에 한 행씩 데이터를 이동하고 각 행에서 4개의 문자열 필드를 읽을 수 있다는 것입니다. 렌더링을 데이터 소스와 분리해 두면 어느 쪽도 방해하지 않고 양쪽을 변경할 수 있습니다
열 형상이 먼저입니다
단일 문자가 그려지기 전에 각 열이 어디에 있는지 결정합니다. 여기에 있는 테이블에는 4개의 열이 있으므로 4개의 왼쪽 가장자리와 알려진 오른쪽 여백이 필요합니다. 빠른 샘플에서 주로 사용하는 방식처럼 모든 TextOut 호출에서 매직 넘버를 하드코딩하는 것은 테이블을 나중에 넓히기 고통스럽게 만드는 바로 그 요인입니다. 가장자리 이름을 한 번 지정하고 왼쪽 하단 원점에서의 포인트로 모든 그리기 호출이 이름으로 해당 가장자리를 참조합니다
const
ColNo = 70; // left edge of the "No." column
ColName = 110; // company name
ColAddr = 300; // street address
ColCity = 480; // city
RowLeft = 50; // table frame: left rule
RowRight = 570; // table frame: right rule
RowStep = 20; // vertical distance between baselines
procedure PrintRow(Page: THPDFPage; Y: Single;
const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
if Shaded then
begin
// A shaded band behind the row. Rectangle takes X, Y, Width, Height.
Page.SetRGBFillColor($00FFF3DD);
Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
Page.Fill;
Page.SetRGBFillColor(clBlack);
end;
Page.TextOut(ColNo, Y, 0, ANo);
Page.TextOut(ColName, Y, 0, AName);
Page.TextOut(ColAddr, Y, 0, AAddr);
Page.TextOut(ColCity, Y, 0, ACity);
end;
여기에 두 가지 세부 사항이 그 가치를 입증합니다. 음영 처리된 대역이 먼저 그려진 다음 그 위에 텍스트가 그려집니다. 왜냐하면 PDF의 칠하기 순서는 z-order이기 때문입니다. 텍스트 뒤에 사각형을 채우면 행을 묻어버리게 됩니다. 그리고 교대로 나타나는 음영은 그 자체를 위한 장식이 아닙니다. 밀도가 높은 보고서에서 눈이 잘못된 줄로 미끄러지는 것을 막는 가장 저렴한 방법이며, 이것이 나중에 루프가 모든 행에서 부울을 반전시키고 Shaded에 바로 전달하는 이유입니다
위의 열 위치는 고정되어 있는데, 이는 귀하가 제어하는 스키마를 가진 보고서에 솔직한 방식입니다. 데이터가 가변적일 때는 추측하는 대신 측정하십시오. HotPDF는 페이지 객체에 텍스트 너비 측정을 노출하므로 PrintRow의 프로덕션 버전은 각 열에서 가장 긴 예상 값을 가져와 선택한 글꼴 크기에서 한 번 측정하고 해당 너비에 여백을 더하여 왼쪽 가장자리를 도출할 수 있습니다. 루틴의 모양은 변하지 않습니다. 상수 소스만 다릅니다
헤더, 규칙, 그리고 이를 소유하는 한 곳
페이지 밖으로 스크롤되었다가 열 레이블 없이 다음 페이지에서 다시 시작되는 테이블은 읽을 수 없습니다. 이 문제를 해결하려면 헤더를 한 번 그리는 것이 아니라 다시 그리는 것으로 취급하는 것입니다. 열 제목과 이를 둘러싸는 수평선을 단일 루틴에 넣고 시작할 때와 새 페이지를 열 때마다 해당 루틴을 호출합니다. 헤더와 본문은 동일한 열 상수를 공유하므로 구조적으로 정렬됩니다
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
// Left: source label and page number. Right: generation time.
Page.SetFont('Arial', [fsItalic], 10);
Page.TextOut(RowLeft, Y, 0, 'customer.db Page ' + IntToStr(PageNo));
Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));
// Two horizontal rules that box the column titles.
Page.MoveTo(RowLeft, Y + 15);
Page.LineTo(RowRight, Y + 15);
Page.MoveTo(RowLeft, Y + 45);
Page.LineTo(RowRight, Y + 45);
Page.Stroke;
// The column titles, in a heavier face so they read as headings.
Page.SetFont('Times New Roman', [fsBold], 12);
Page.SetRGBFillColor(clNavy);
PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
Page.SetRGBFillColor(clBlack);
Y := Y + RowStep + 45; // advance past the boxed header before the first body row
end;
DrawHeader는 참조로 Y를 취하여 앞으로 이동시킨다는 점에 유의하십시오. 호출자는 헤더의 높이가 얼마나 되는지 기억할 필요가 없습니다. 헤더를 그리는 루틴이 바로 아는 루틴입니다. 이 단일 소유권 규칙이 나중에 헤더 밴드에 로고나 필터 요약을 추가할 때 레이아웃이 이동하지 않도록 하는 것입니다. 본문 루프는 무감각하게 유지됩니다. 현재 Y가 가리키는 곳에서 행을 계속 그리기만 하면 됩니다
규칙 자체는 목록과 테이블의 차이점입니다. 수직 열 구분 기호는 x축에 적용되는 동일한 아이디어입니다. 각 열 가장자리의 MoveTo / LineTo / Stroke는 상단 규칙에서 페이지의 마지막 행 하단까지 이어집니다. 샘플은 읽기 쉽도록 가로 선을 유지하지만 프로덕션 단계는 열 상수가 있으면 기계적입니다
커서 루프가 페이지 나누기를 소유합니다
그리는 것은 절반의 쉬운 부분입니다. 장난감과 보고서를 구분하는 절반은 페이지 매기기입니다. 즉 행을 그리기 전에 여전히 맞는지 확인하고, 그렇지 않을 때 새 헤더가 있는 새 페이지를 시작하는 것입니다. 이 결정은 정확히 한 곳, 데이터를 걷는 루프에 속하며 다른 곳에는 속하지 않습니다
var
Pdf: THotPDF;
Page: THPDFPage;
Y: Single;
PageNo: Integer;
Shaded: boolean;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'CustomerReport.pdf';
Pdf.BeginDoc;
Page := Pdf.CurrentPage;
// Report title, once, at the top of the first page.
Page.SetFont('Arial', [fsBold], 24);
Page.TextOut(200, 800, 0, 'Customer Report');
PageNo := 1;
Y := 760;
DrawHeader(Page, Y, PageNo);
Shaded := False;
CustomerTable.First;
while not CustomerTable.Eof do
begin
// Out of room? Open a new page and repeat the header there.
if Y < 60 then
begin
Pdf.AddPage;
Page := Pdf.CurrentPage; // AddPage moves CurrentPage forward
Inc(PageNo);
Y := 760;
DrawHeader(Page, Y, PageNo);
end;
Shaded := not Shaded;
Page.SetFont('Arial', [], 10); // SetFont must be reissued on every new page
PrintRow(Page, Y,
VarToStr(CustomerTable['CustNo']),
VarToStr(CustomerTable['Company']),
VarToStr(CustomerTable['Addr1']),
VarToStr(CustomerTable['City']),
Shaded);
Y := Y - RowStep;
CustomerTable.Next;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
두 가지 좌표 사실이 전체 루프를 주도합니다. PDF는 왼쪽 하단 모서리에서 위쪽으로 y를 측정하므로 행은 매번 Y에서 RowStep을 빼서 페이지 아래로 행진하고 가득 찬 페이지 테스트는 Y가 상단 위가 아니라 하단 여백 아래로 떨어질 때 실행됩니다. 방향을 거꾸로 맞추면 루프가 가득 찬 페이지 공간이 있다고 생각하는 동안 첫 번째 행이 하단 가장자리 밖으로 인쇄됩니다
다른 사실은 거의 모든 사람을 한 번은 사로잡습니다. AddPage는 새 페이지를 만들고 CurrentPage를 해당 페이지로 재지정하지만 이월되는 것은 아무것도 없습니다. 글꼴, 채우기 색상, 위치 모두 없습니다. 이것이 모든 AddPage 후에 CurrentPage에서 Page를 다시 읽고, 본문 행 전에 SetFont를 다시 발행하는 이유입니다. 다시 읽기를 건너뛰면 방금 남겨둔 페이지에 계속 그리게 되고, 글꼴을 건너뛰면 뷰어가 의지하는 기본값으로 새 페이지가 렌더링됩니다
테이블 내보내기 도구를 손상시키는 사례
대부분의 테이블 버그는 깔끔한 몇 십 개의 행이 있는 행복한 경로에는 나타나지 않습니다. 그들은 가장자리에 있으며, 가장자리가 어디에 있는지 알면 가장자리는 테스트하기 저렴합니다
- 빈 데이터세트. 빈 0개 행에 대한 루프는 헤더와 그 아래에 아무것도 없는 페이지를 생성하는데, 이는 최소한 의도적인 것처럼 보입니다. 헤더가 없는 빈 페이지는 실패한 것처럼 보입니다. 출시하기 전에 어느 것을 원하는지 결정하십시오
- 경계에 정확히 닿는 행. 마지막 행이 여백 위로 한 단계 위에 있는 보고서를 생성한 다음, 다음 행이 여백 아래로 한 단계 아래에 있는 보고서를 생성합니다. 1씩 차이 나는 페이지 매기기는 데이터가 정확히 잘못된 길이일 때까지 숨겨집니다
- 지나치게 긴 값. 열보다 넓은 회사 이름은 다음 열로 넘어갑니다. 필드를 측정하고 정책을 결정하십시오. 두 번째 줄로 감싸기, 자르기 또는 줄임표로 자르기입니다. 침묵은 정책이 아닙니다
- Null 필드.
TextOut으로 null을 직접 읽어오면 어떻게 변환하느냐에 따라Null이라는 리터럴 텍스트로 나타나거나 빈 공간으로 나타날 수 있습니다. Variant 변환이 귀하를 위해 렌더링을 선택하도록 두지 말고 의도적으로 렌더링을 선택하십시오
완료되었다고 부르기 전에 둘 이상의 뷰어를 통해 결과를 실행하십시오. 글꼴 교체 및 자르기는 렌더러마다 다르게 작동하며 한 PDF 리더에서 정사각형으로 보이는 테이블이 다른 PDF 리더에서는 열이 정렬되지 않거나 도시가 잘릴 수 있습니다. 반복된 헤더, 행 음영, 여백이 이동에도 유지되는지, 데이터가 경계를 넘은 후에도 페이지 번호가 연속적인지 확인하십시오
시각적 보고서 디자인 도구에 기대는 대신 격자를 직접 그리는 것은 코드가 더 많고 절충안의 가치는 명확하게 지정할 가치가 있습니다. 모든 좌표를 소유하게 되는데, 이는 모든 머신에서 동일하게 렌더링해야 하는 서버 측 일괄 작업, 송장, 감사 내보내기에 정확히 원하는 것이며, 한 번 사용하는 내부 목록의 경우 정확히 피하고 싶은 오버헤드입니다. 전자의 경우, 보고서가 책상에서 본 것과 동일하게 프로덕션 환경에서도 보여야 할 때 제어 비용이 상쇄됩니다
Rectangle, MoveTo, LineTo 호출을 먼저 자체적으로 처리하고 싶다면 위의 규칙과 음영 밴드는 캔버스 그리기 연습에서 다루는 것과 동일한 벡터 및 색상 프리미티브에 의존합니다. 여기에 사용된 그리기 기본 요소는 Delphi 및 C++Builder용 HotPDF Component의 일부입니다