Để xuất TDataSet hoặc báo cáo sang PDF trong Delphi, losLab PDF Library cung cấp hai hướng đi. PDFlibTableExport chuyển đổi bất kỳ TDataSet nào — truy vấn FireDAC, ClientDataSet, bảng trong bộ nhớ — thành một bảng PDF được phân trang, và ba cầu nối (bridge) trong phần Addons sẽ chuyển giao báo cáo FastReport, QuickReport hoặc ReportBuilder đã chuẩn bị sẵn cho cùng một trình ghi PDF. Cả hai đều tạo ra tệp PDF thực tế mà không cần trình điều khiển máy in hay cửa sổ hiển thị
Hai vấn đề này nhìn bề ngoài có vẻ giống nhau nhưng thực tế lại không phải vậy. Một DBGrid hoặc kết quả truy vấn thô không có bố cục riêng, vì vậy việc xuất dữ liệu đồng nghĩa với việc tự thiết lập: cột, tiêu đề trang, ngắt trang. Trong khi đó, tài liệu FastReport hoặc ReportBuilder vốn đã mang sẵn một bố cục được thiết kế, nên việc xuất dữ liệu nghĩa là tái hiện một cách trung thực các lệnh vẽ của người khác vào không gian PDF. losLab PDF Library phân tách các tác vụ này thành các unit khác nhau chính vì các chế độ lỗi của chúng khác nhau, và phần còn lại của bài viết này sẽ đi qua từng trường hợp cũng như những điểm mà chúng không còn đáng tin cậy
Làm thế nào để xuất TDataSet sang PDF trong Delphi?
PDFlibTableExport chuyển đổi một dataset thành một bảng chỉ với một lệnh gọi. Bộ xuất dữ liệu sẽ duyệt qua danh sách trường một lần, tự động bỏ qua các trường blob nhị phân — ftBlob, ftGraphic, ftBytes và các trường tương tự không có văn bản ô hữu dụng — căn phải các cột số, và vẽ các đường sọc ngựa vằn tùy chọn trên băng tiêu đề đã được định dạng. Bên dưới, nó xây dựng lưới bằng CreateTable, điền vào các ô bằng SetTableCellContent, và kết xuất bằng DrawTableRows, cũng chính là API Table công khai mà bạn có thể tự điều khiển thủ công. Lớp bọc tiện ích PDFlibExportDataSet sẽ tự cấu hình đơn vị milimet và điểm gốc trên cùng bên trái cho bạn, vì vậy người gọi chỉ cần cung cấp một trang
uses
Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
PDFlibrary, PDFlibTableExport;
var
MemTable: TFDMemTable;
PDF: TPDFlib;
Options: TPDFlibTableExportOptions;
begin
MemTable := TFDMemTable.Create(nil);
PDF := TPDFlib.Create;
try
MemTable.LoadFromFile('customer.FDS'); // any TDataSet works here
PDF.SetOrigin(1);
PDF.SetMeasurementUnits(1); // millimetres
PDF.SetPageSize('A4');
PDF.AddStandardFont(4);
Options := DefaultTableExportOptions;
Options.Title := 'Customers';
Options.ColumnWidth := 32; // 11 fields fit on A4 at 32 mm
Options.RepeatHeader := True; // redraw the header on each page
PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
finally
PDF.Free;
MemTable.Free;
end;
end;
Việc tự động bỏ qua blob là chế độ mặc định chứ không phải là sự ép buộc. Khi bạn cần kiểm soát từng trường — nhãn cột tùy chỉnh, chiều rộng hẹp hơn, hoặc loại bỏ một cột khóa nội bộ không phải là blob — hãy tạo trực tiếp một TPDFlibTableExporter và liên kết sự kiện OnFieldFilter của nó, sự kiện này kích hoạt một lần cho mỗi trường và cung cấp cho bạn một đặc tả có thể thay đổi. Đặt Include thành False để bỏ trường, hoặc đặt ColumnWidth and DisplayLabel để ghi đè lên các giá trị mặc định. Móc nối đó cũng là nơi bạn loại trừ một cột memo rộng mà bạn không muốn làm phình trang giấy
Phân trang: truyền trạng thái vẽ giữa các trang
DrawTableRows mang trạng thái phân trang, và việc hiểu rõ cơ chế hoạt động của nó là toàn bộ vấn đề. Bạn gọi nó với hàng đầu tiên và hàng cuối cùng; truyền hàng cuối cùng nhỏ hơn 1 nghĩa là vẽ đến hết bảng, bị giới hạn bởi chiều cao bạn đã cung cấp. Hàm này trả về chiều cao thực tế mà nó đã vẽ, và GetTableLastDrawnRow báo cáo hàng cuối cùng đã vừa vặn. Cặp giá trị đó là trạng thái bạn chuyển từ trang này sang trang tiếp theo: khi hàng được vẽ cuối cùng chưa đạt đến tổng số hàng, bạn mở một trang mới và tiếp tục từ hàng sau nó. Không có gì được đo lường lại — phần tiếp theo sẽ bắt đầu chính xác từ nơi cuộc gọi trước đó dừng lại
// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
DrawHeight := Y - Options.BottomMargin;
// Last row = 0 means "draw to the end", bounded by DrawHeight
PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
LastDrawn := PDF.GetTableLastDrawnRow(TableID);
if LastDrawn >= TotalRows then
Break; // the whole table fit on this page
if LastDrawn < Row then
Break; // safety: no forward progress, bail out
PDF.NewPage;
Row := LastDrawn + 1; // continue from the first undrawn row
Y := PageHeight - Options.Top;
end;
Vòng lặp cũng là nơi chứa tiêu đề lặp lại. Trên mỗi trang tiếp theo, bộ xuất dữ liệu có tùy chọn vẽ lại hàng 1 — tiêu đề — trước các hàng dữ liệu, và nó sử dụng chiều cao được trả về bởi lệnh gọi DrawTableRows đầu tiên đó để bù trừ cho phần thân bên dưới nó. Đây là lý do tại sao DrawTableRows trả về một chiều cao thay vì một tọa độ hàng tiếp theo: giá trị trả về là thứ giúp bạn xếp chồng tiêu đề được vẽ lại và phần thân tiếp tục mà không cần mã hóa cứng kích thước của bất kỳ phần nào
Làm thế nào để xuất báo cáo FastReport hoặc ReportBuilder sang PDF?
Các cầu nối báo cáo áp dụng cách tiếp cận ngược lại: chúng không bao giờ tự tạo bố cục mà tái hiện lại một bố cục có sẵn. Mỗi cầu nối gắn kết với công cụ của nó tại điểm xuất gốc của công cụ đó. PDFlibFRExport là lớp con của TfrxCustomExportFilter và nhận các đối tượng memo, hình ảnh, hình dạng cũng như dòng của FastReport, dịch từng đối tượng thành một phần tử cơ bản của PDFlibPas. PDFlibRBDevice là lớp con của TppFileDevice và duyệt qua danh sách DrawCommand của ReportBuilder. PDFlibQRExport đi theo một con đường hoàn toàn thứ ba mà phần tiếp theo sẽ đề cập. Cả ba đều chạy không giao diện (headless), đó là lý do chính để sử dụng chúng trên máy chủ
Chạy headless không phải là miễn phí, và FastReport là một bài học cảnh báo. TfrxReport.Export định tuyến qua các trang xem trước, các trang này tham chiếu thuộc tính ShowDialog của bộ lọc, và thuộc tính đó kế thừa giá trị mặc định là True. Trên một máy không có màn hình tương tác, hộp thoại modal trả về kết quả hủy (cancel) và quá trình xuất tệp âm thầm thất bại. Hãy đặt ShowDialog thành False trước khi bạn gọi Export và báo cáo sẽ tự động kết xuất một cách im lặng. ReportBuilder có các nút chuyển đổi song song — AllowPrintToFile and ShowPrintDialog — và QuickReport, vốn điều khiển mọi thứ thông qua Prepare, hoàn toàn không cần triệt tiêu hộp thoại
var
Exporter: TPDFlibFRExport;
begin
Report.PrepareReport; // build the pages first
Exporter := TPDFlibFRExport.Create(nil);
try
Exporter.FileName := 'invoice.pdf';
Exporter.ShowDialog := False; // headless: skip the modal, no cancel
Report.Export(Exporter);
finally
Exporter.Free;
end;
end;
Ba công cụ, ba hệ tọa độ
Mỗi cầu nối thực hiện các phép tính tọa độ riêng của mình, bởi vì mỗi công cụ đo lường thế giới theo cách khác nhau. PDFlibFRExport xử lý các vị trí đối tượng FastReport dưới dạng pixel ở độ phân giải 96 PPI và tỷ lệ theo 96/25.4 để chuyển đổi sang milimet. PDFlibRBDevice đọc các lệnh vẽ của ReportBuilder theo đơn vị phần nghìn mm và chia cho 1000, và nó tự đăng ký qua ppRegisterDevice, vì vậy chỉ cần đặt ppReport.DeviceType thành 'PDFlibPas' và gọi Print là đủ để định tuyến đầu ra qua nó. PDFlibQRExport hoàn toàn không thực hiện phép toán tọa độ nào: một trang QuickReport được chuẩn bị sẵn đã là một EMF metafile, vì thế cầu nối lưu mỗi trang vào một luồng dữ liệu và nạp nó vào ImportEMFFromStream, cho phép đường dẫn kết xuất ngữ cảnh thiết bị và metafile của thư viện đặt mọi glyph và quy tắc. Ngược lại, các cầu nối FastReport và ReportBuilder phát ra các phần tử vẽ vectơ gốc và văn bản gốc
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1);
// Prepares the report and appends every page in one call; the bridge
// turns each page's EMF into PDF through ImportEMFFromStream.
PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
finally
PDF.Free;
end;
end;
Nơi sự trung thực kết thúc
Hãy nắm rõ các ranh giới trước khi bạn giao một quy trình công việc cho các cầu nối này. Bộ xuất dataset âm thầm bỏ qua các cột nhị phân, vì vậy một báo cáo cần hiển thị hình ảnh nhúng đòi hỏi một cách tiếp cận khác so với một bảng thẳng. Đường dẫn QuickReport, vì đi qua EMF, sẽ chuyển văn bản thành các glyph vectơ — trang hiển thị đúng nhưng không chứa văn bản có thể chọn hoặc tìm kiếm và không có cấu trúc PDF được gắn thẻ để hỗ trợ tiếp cận. FastReport và ReportBuilder giữ lại văn bản thực tế, nhưng các trình xử lý được phân loại chỉ bao gồm các chế độ xem phổ biến — memo, hình ảnh, hình dạng, dòng — và quay về hộp giới hạn (bounding box) hoặc bỏ qua những chế độ xem lạ lẫm, do đó một báo cáo dựa nhiều vào văn bản định dạng (rich text), mã vạch hoặc tô màu chuyển sắc sẽ bị mất chi tiết. Không có điều nào trong số này là lỗi; đó là giới hạn thực tế của một lớp dịch thuật
Có một lưu ý vận hành quan trọng hơn cả. Không có công cụ nào trong số ba công cụ này đi kèm bên trong losLab PDF Library, và các cầu nối được loại trừ một cách có chủ đích khỏi bản dựng chính — chúng chỉ biên dịch từ một dự án đã có sẵn FastReport VCL 6.x, QuickReport 8 hoặc ReportBuilder 20 trên đường dẫn tìm kiếm của nó. Các bề mặt metafile và DrawCommand mà chúng nhắm tới vẫn ổn định qua nhiều phiên bản công cụ chính, nhưng bạn phải tự đảm bảo sự phù hợp về phiên bản. Thực hiện đúng điều đó và hai con đường này sẽ bao phủ toàn bộ phạm vi, từ kết xuất DBGrid đột xuất cho đến một hóa đơn được thiết kế. Cả bộ xuất bảng và các cầu nối báo cáo đều đi kèm với losLab PDF Library dành cho Delphi và C++Builder