dataset הוא שורות ועמודות; עמוד PDF הוא רשת קואורדינטות ריקה ללא מושג של אף אחד מהם. גישור על הפער הזה הוא כל העבודה כאן. אין קריאת DrawTable ב-HotPDF שלוקחת dataset ומוסרת לך רשת מעוצבת. מה שאתה מקבל במקום זה הם הפרימיטיבים שמהם מורכבת רשת: TextOut כדי למקם מחרוזת בנקודה, SetFont כדי לבחור את הגופן שלה, Rectangle ו-Fill כדי להצליל רצועה, ו-MoveTo / LineTo / Stroke כדי לצייר קווים. יצואן טבלאות עובד הוא המשמעת של הפיכת חשיבה של שורות ועמודות לקואורדינטות x ו-y מפורשות, ואז שמירה על הקואורדינטות הללו כנות כאשר הנתונים חורגים מתחתית העמוד
הדוגמה הבאה מדווחת על רשומות לקוחות, אך שום דבר בקוד הציור אינו יודע או אכפת לו מהיכן מגיעות השורות. המקור השתמש ב-TTable מדור קודם; שאילתת FireDAC, או dataset בזיכרון, או מערך רגיל של רשומות מזינים את אותן שגרות ללא שינוי. מה שחשוב הוא שאתה יכול לעבור על הנתונים שורה אחת בכל פעם ולקרוא ארבעה שדות מחרוזת מכל אחת. שמור על הרינדור נפרד ממקור הנתונים ותוכל לשנות כל צד מבלי להפריע לשני
גיאומטריית העמודות קודמת לכל
לפני שתו בודד מצויר, החלט היכן כל עמודה יושבת. לטבלה יש כאן ארבע עמודות, לכן היא זקוקה לארבעה קצוות שמאליים ושוליים ימניים ידועים. קידוד קשיח של מספר קסם בכל קריאת 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 יכולה לקחת את הערך הצפוי הארוך ביותר בכל עמודה, למדוד אותו פעם אחת בגודל הגופן שנבחר, ולהפיק את הקצוות השמאליים מרוחבים אלה בתוספת מרזב (gutter). צורת השגרה אינה משתנה; רק המקור של הקבועים משתנה
הכותרת, הקווים, ומקום אחד שבבעלותו הם
טבלה שגולשת מדף אחד וממשיכה בבא אחריו ללא תוויות עמודות אינה קריאה. התיקון הוא להתייחס לכותרת כאל משהו שאתה מצייר מחדש, לא משהו שאתה מצייר פעם אחת. שים את כותרות העמודות והקווים האופקיים שממסגרים אותן בשגרה אחת, וקרא לשגרה זו גם בהתחלה ושוב בכל פעם שאתה פותח עמוד חדש. מכיוון שהכותרת והגוף חולקים את אותם קבועי עמודות, הם מסתדרים בשורה מעצם בנייתם
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 לפי התייחסות (by reference) ומזיז אותו קדימה. הקורא לעולם אינו צריך לזכור כמה גבוהה הכותרת; השגרה שמציירת אותה היא השגרה שיודעת. כלל הבעלות היחיד הזה הוא מה שמונע מהפריסה להיסחף כאשר אתה מוסיף מאוחר יותר לוגו או סיכום מסנן לרצועת הכותרת. לולאת הגוף נשארת אדישה. היא פשוט ממשיכה לצייר שורות מכל מקום שאליו Y מצביע כרגע
הקווים עצמם הם ההבדל בין רשימה לטבלה. מפרידי עמודות אנכיים הם אותו רעיון המיושם על ציר ה-x: MoveTo / LineTo / Stroke בכל קצה עמודה, הרצים מהקו העליון ועד לתחתית השורה האחרונה בעמוד. הדוגמה מסתפקת בקווים אופקיים כדי להישאר קריאה, אך שלב הייצור הוא מכני ברגע שקבועי העמודות קיימים
לולאת הסמן מחזיקה במעבר העמוד (page break)
הציור הוא החצי הקל. החצי שמפריד צעצוע מדוח הוא עימוד (pagination): לדעת, לפני שאתה מצייר שורה, האם היא עדיין נכנסת, והתחלת עמוד חדש ורענן עם כותרת חדשה כאשר היא לא. החלטה זו שייכת למקום אחד בדיוק, הלולאה שעוברת על הנתונים, ולא לשום מקום אחר
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 מוצא מחדש לפני שורות הגוף. דלג על הקריאה מחדש ותמשיך לצייר על העמוד שהרגע השארת מאחור; דלג על הגופן והעמוד החדש ירונדר בכל ברירת מחדל שהצופה ייסוג אליה
המקרים ששוברים יצואן טבלאות
רוב באגי הטבלאות אינם מופיעים בנתיב השמח של כמה עשרות שורות מסודרות. הם חיים בקצוות, והקצוות זולים לבדיקה ברגע שאתה יודע היכן הם נמצאים
- ערכות נתונים ריקות (Empty datasets). לולאה על אפס שורות מייצרת עמוד עם כותרת ושום דבר תחתיה, שלפחות נראה מכוון. עמוד ריק ללא כותרת נראה כמו כישלון. החלט מה אתה רוצה לפני שאתה משחרר ללקוחות
- השורה שנוחתת בדיוק על הגבול. צור דוח שהשורה האחרונה שלו יושבת צעד אחד מעל השוליים, ואז אחד שהשורה הבאה שלו היא צעד אחד מתחתיהם. עימוד שגוי באחד (Off-by-one) מסתתר עד שהנתונים הם בדיוק באורך הלא נכון
- ערכים ארוכים מדי. שם חברה רחב יותר מהעמודה שלו יגלוש אל תוך העמודה הבאה. מדוד את השדה והחלט על מדיניות: גלישה לשורה שנייה, חיתוך, או קיטוע עם שלוש נקודות. שתיקה אינה מדיניות
- שדות Null. קריאת null ישר לתוך
TextOutיכולה לצוץ כטקסט המילוליNullאו כריק, תלוי איך אתה ממיר אותו. בחר את הרינדור במכוון במקום לאפשר להמרת הווריאנט (variant conversion) לבחור עבורך
הרץ את התוצאה דרך יותר מצופה (viewer) אחד לפני שאתה קורא לזה גמור. החלפת גופנים וחיתוך מתנהגים שונה בין מרנדרים (renderers), וטבלה שנראית מרובעת בקורא PDF אחד יכולה להראות עמודה לא מיושרת או עיר חתוכה באחר. ודא שהכותרת החוזרת, הצללת השורה והשוליים שורדים את המעבר, ושהמספור של העמודים נשאר רציף לאחר שהנתונים חוצים גבול
ציור הרשת בעצמך במקום להישען על מעצב דוחות חזותי דורש יותר קוד, וכדאי לציין את הפשרה בצורה ברורה: בבעלותך כל קואורדינטה, שזה בדיוק מה שאתה רוצה עבור עבודות אצווה (batch jobs) בצד השרת, חשבוניות וייצוא ביקורת שחייבים להיות מרונדרים באופן זהה בכל מכונה, ובדיוק התקורה שהיית מעדיף להימנע ממנה עבור רשימה פנימית חד-פעמית. עבור הראשון, השליטה משלמת על עצמה בפעם הראשונה שדוח צריך להיראות בייצור אותו הדבר כפי שנראה על שולחנך
הקווים והרצועות המוצללות לעיל נשענים על אותם פרימיטיבים של וקטור וצבע המכוסים בהדרכת הציור על קנבס, אם אתה רוצה שיתייחסו לקריאות ה-Rectangle, MoveTo, ו-LineTo בפני עצמן קודם. פרימיטיבי הציור המשמשים כאן הם חלק מרכיב HotPDF עבור Delphi ו-C++Builder