ชุดข้อมูล (dataset) ประกอบด้วยแถวและคอลัมน์ ส่วนหน้า PDF คือกริดพิกัดว่างเปล่าที่ไม่มีแนวคิดของทั้งสองอย่าง การเชื่อมโยงช่องว่างนั้นคือหน้าที่ทั้งหมดในที่นี้ HotPDF ไม่มีคำสั่ง DrawTable ที่รับชุดข้อมูลแล้วส่งคืนกริดที่จัดรูปแบบแล้วให้คุณ สิ่งที่คุณได้มาคือองค์ประกอบพื้นฐานที่ใช้สร้างกริด: TextOut เพื่อวางสตริงที่จุด ๆ หนึ่ง, SetFont เพื่อเลือกแบบอักษร, Rectangle และ Fill เพื่อระบายสีแถบ และ MoveTo / LineTo / Stroke เพื่อวาดเส้น ตัวส่งออกตารางที่ทำงานได้จริงต้องใช้ความมีระเบียบวินัยในการเปลี่ยนแนวคิดของแถวและคอลัมน์ให้เป็นพิกัด x และ y ที่ชัดเจน และรักษาพิกัดเหล่านั้นให้ถูกต้องเมื่อข้อมูลยาวเลยขอบล่างของหน้ากระดาษ
ตัวอย่างต่อไปนี้เป็นรายงานข้อมูลลูกค้า แต่ไม่มีโค้ดวาดส่วนใดที่รู้หรือสนใจว่าแถวข้อมูลมาจากไหน ตัวอย่างต้นฉบับใช้ TTable รุ่นเก่า แต่ไม่ว่าจะเป็นคิวรี FireDAC ชุดข้อมูลในหน่วยความจำ หรืออาร์เรย์เรคอร์ดธรรมดาก็สามารถส่งให้รูทีนเดียวกันนี้ได้โดยไม่ต้องแก้ไข สิ่งสำคัญคือคุณสามารถวนลูปข้อมูลได้ทีละแถวและอ่านฟิลด์สตริงสี่ฟิลด์จากแต่ละแถวได้ หากแยกระบบการวาดออกจากแหล่งข้อมูล คุณก็สามารถเปลี่ยนแปลงฝั่งใดฝั่งหนึ่งได้โดยไม่กระทบอีกฝั่ง
รูปทรงของคอลัมน์ต้องมาก่อน
ก่อนจะวาดตัวอักษรตัวแรก ให้ตัดสินใจก่อนว่าแต่ละคอลัมน์จะอยู่ที่ไหน ตารางนี้มีสี่คอลัมน์ จึงต้องมีขอบซ้ายสี่ด้านและระยะขอบขวาที่แน่นอน การฝังค่าตัวเลขลงไปตรง ๆ ในทุกครั้งที่เรียก 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;
มีรายละเอียดสองจุดที่สำคัญที่นี่ แถบแรเงาจะถูกวาดก่อน จากนั้นจึงวาดข้อความทับลงไป เนื่องจากลำดับการระบายคือ z-order ใน PDF: หากคุณเติมสีสี่เหลี่ยมหลังจากวาดข้อความ ข้อความของแถวนั้นจะถูกฝังทับ และการสลับสีแรเงาไม่ได้มีไว้เพื่อความสวยงามเพียงอย่างเดียว ในรายงานที่หนาแน่น มันเป็นวิธีที่ประหยัดที่สุดในการป้องกันไม่ให้สายตาเลื่อนผิดบรรทัด ซึ่งเป็นเหตุผลที่การวนลูปในภายหลังจะสลับค่าบูลีนในทุกแถวและส่งตรงเข้าไปที่ 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 แบบ reference และเลื่อนค่าไปข้างหน้า ผู้เรียกไม่ต้องจำว่าส่วนหัวตารางสูงเท่าไหร่ รูทีนที่วาดมันคือรูทีนที่รู้ กฎการรับผิดชอบจุดเดียวนี่แหละที่จะช่วยให้เลย์เอาต์ไม่ผิดเพี้ยนเวลาคุณเพิ่มโลโก้หรือผลรวมการกรองลงในแถบส่วนหัวในภายหลัง ลูปสำหรับส่วนเนื้อหาก็ไม่ต้องสนใจอะไร มันแค่ทำหน้าที่วาดแถวต่อไปเรื่อย ๆ จากตำแหน่งใดก็ตามที่ 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 ขึ้นด้านบนจากมุมซ้ายล่าง ดังนั้นแถวต่าง ๆ จะเดินลงมาตามหน้ากระดาษโดยการลบ RowStep ออกจาก Y ทุกครั้ง และการตรวจสอบว่าหน้าเต็มหรือไม่จะทำงานเมื่อ Y ต่ำกว่าระยะขอบล่าง ไม่ใช่พุ่งขึ้นเหนือขอบบน หากทำทิศทางกลับกัน แถวแรกจะพิมพ์หลุดขอบล่างในขณะที่ลูปเข้าใจว่ายังมีพื้นที่ว่างเหลือทั้งหน้า
ความจริงอีกประการหนึ่งคือสิ่งที่มักดักทุกคนให้พลาด AddPage สร้างหน้าใหม่และชี้ CurrentPage ไปยังหน้านั้น แต่มันไม่ได้ยกสิ่งใดติดมาด้วย: ไม่มีแบบอักษร ไม่มีสีเติม ไม่มีตำแหน่ง นี่คือเหตุผลว่าทำไมจึงต้องอ่าน Page จาก CurrentPage ใหม่อีกครั้งทุกครั้งหลังจากทำ AddPage และเหตุใดจึงต้องเรียก SetFont ใหม่ก่อนที่จะเริ่มพิมพ์แถวในส่วนเนื้อหา หากข้ามการอ่านใหม่ไป คุณจะยังคงวาดลงบนหน้าที่เพิ่งจากมา และหากข้ามการตั้งฟอนต์ไป หน้าใหม่จะเรนเดอร์ด้วยค่าเริ่มต้นใด ๆ ก็ตามที่โปรแกรมอ่านใช้
กรณีที่ทำให้ตัวส่งออกตารางมีปัญหา
ข้อผิดพลาดเกี่ยวกับตารางส่วนใหญ่มักไม่ค่อยแสดงอาการเมื่อข้อมูลมีแค่ไม่กี่สิบแถวที่เป็นระเบียบเรียบร้อย มันซ่อนอยู่ที่ขอบ และขอบเหล่านั้นตรวจสอบได้ไม่ยากเมื่อคุณรู้ว่ามันอยู่ที่ไหน
- ชุดข้อมูลว่างเปล่า ลูปที่ทำงานบนศูนย์แถวจะสร้างหน้ากระดาษที่มีส่วนหัวแต่ไม่มีอะไรอยู่ด้านล่าง ซึ่งอย่างน้อยก็ดูเหมือนว่าตั้งใจให้เป็นแบบนั้น หน้ากระดาษเปล่าที่ไม่มีส่วนหัวจะดูเหมือนความล้มเหลว ตัดสินใจเลือกแบบที่ต้องการก่อนจะเปิดใช้งานระบบจริง
- แถวที่ตกกระทบเส้นขอบพอดิบพอดี ลองสร้างรายงานที่แถวสุดท้ายอยู่สูงกว่าเส้นขอบ 1 สเต็ป จากนั้นรายงานถัดไปมีอีกแถวที่ต่ำกว่าเส้นขอบ 1 สเต็ป ปัญหาการแบ่งหน้าที่คลาดเคลื่อนไปหนึ่งช่องจะซ่อนตัวอยู่จนกว่าความยาวข้อมูลจะไม่พอดีกันเป๊ะ ๆ
- ค่าที่ยาวเกินไป ชื่อบริษัทที่กว้างกว่าคอลัมน์จะวิ่งทะลุเข้าไปในคอลัมน์ถัดไป ลองวัดฟิลด์ดูและตัดสินใจเลือกนโยบาย: ตัดขึ้นบรรทัดใหม่, ตัดส่วนเกินทิ้ง, หรือตัดทิ้งด้วยเครื่องหมายจุดไข่ปลา (...) การปล่อยผ่านเงียบ ๆ ไม่ใช่นโยบาย
- ฟิลด์ที่ค่าเป็น Null การอ่านค่า Null เข้าไปใน
TextOutตรง ๆ อาจแสดงออกมาเป็นข้อความว่าNullอย่างจัง หรืออาจจะเป็นที่ว่าง ขึ้นอยู่กับวิธีที่คุณแปลงค่า เลือกการแสดงผลอย่างมีสติ แทนที่จะปล่อยให้ฟังก์ชันแปลงตัวแปรกำหนดให้คุณ
ลองรันผลลัพธ์ผ่านโปรแกรมอ่านมากกว่าหนึ่งตัวก่อนจะสรุปว่าเสร็จสมบูรณ์ การแทนที่แบบอักษรและการตัดขอบจะแสดงผลต่างกันไปในแต่ละตัวเรนเดอร์ และตารางที่ดูเป็นสี่เหลี่ยมจัตุรัสในโปรแกรมอ่าน PDF ตัวหนึ่งอาจแสดงให้เห็นคอลัมน์ที่เยื้องกันหรือเมืองที่ถูกตัดในโปรแกรมอ่านอีกตัว ยืนยันว่าส่วนหัวที่ซ้ำกัน การแรเงาแถว และระยะขอบนั้นยังคงอยู่ และหมายเลขหน้าจะต้องต่อเนื่องกันแม้ข้อมูลจะข้ามเส้นแบ่งหน้าไปแล้ว
การวาดกริดด้วยตัวเองแทนที่จะพึ่งพาโปรแกรมออกแบบรายงานแบบเห็นภาพนั้นต้องเขียนโค้ดเยอะกว่า และการแลกเปลี่ยนนี้ก็คุ้มค่าที่จะระบุให้ชัดเจน: คุณสามารถควบคุมได้ทุกพิกัด ซึ่งเป็นสิ่งที่คุณต้องการเป๊ะ ๆ สำหรับงานประมวลผลเป็นชุดจากฝั่งเซิร์ฟเวอร์ ใบแจ้งหนี้ และการส่งออกข้อมูลการตรวจสอบที่ต้องเรนเดอร์ออกมาเหมือนกันเป๊ะบนทุกเครื่อง และนี่คือภาระหน้าที่ที่คุณคงไม่อยากเจอสำหรับการดูรายการภายในแบบครั้งเดียวทิ้ง สำหรับงานแบบแรก การที่คุณควบคุมมันได้จะคุ้มค่าคุ้มราคาก็ตอนที่รายงานออกมาเหมือนกับตอนที่คุณดูบนเครื่องของคุณทุกประการ
กฎและแถบแรเงาด้านบนนี้ใช้รูปแบบเวกเตอร์และสีพื้นฐานเดียวกันกับที่ครอบคลุมไว้ในคำแนะนำการวาดแคนวาส หากคุณต้องการศึกษาเกี่ยวกับการเรียกใช้ Rectangle, MoveTo และ LineTo แยกต่างหาก องค์ประกอบพื้นฐานสำหรับการวาดที่ใช้ที่นี่เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder