مجموعة البيانات عبارة عن صفوف وأعمدة، أما صفحة PDF فهي شبكة إحداثيات فارغة لا تعرف أيًا منهما. سد هذه الفجوة هو جوهر المهمة هنا. لا توجد دالة DrawTable في HotPDF تأخذ مجموعة بيانات وتعيد لك شبكة منسقة. ما تحصل عليه بدلًا من ذلك هو اللبنات التي تتكون منها الشبكة: 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;
تفصيلان يجعلان ذلك مفيدًا هنا. يُرسم الشريط المظلل أولًا، ثم النص فوقه، لأن ترتيب الرسم هو ترتيب الطبقات في 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 بالمرجع ويدفعه إلى الأمام. لا يحتاج المستدعي أبدًا إلى تذكر ارتفاع الرأس؛ فالروتين الذي يرسمه هو الروتين الذي يعرفه. قاعدة الملكية الواحدة هذه هي ما يمنع التخطيط من الانجراف عندما تضيف لاحقًا شعارًا أو ملخص مرشح إلى شريط الرأس. تظل حلقة المحتوى غير معنية، وتواصل فقط رسم الصفوف من الموضع الذي تشير إليه 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 من جديد قبل صفوف المحتوى. إذا تركت إعادة القراءة فستواصل الرسم على الصفحة التي غادرتها للتو؛ وإذا تركت الخط فستُعرض الصفحة الجديدة بالافتراضي الذي يعود إليه العارض.
الحالات التي تكسر مصدّر الجداول
معظم أخطاء الجداول لا تظهر في المسار العادي لبضعة عشرات من الصفوف المرتبة. إنها تعيش عند الحواف، والحواف سهلة الاختبار عندما تعرف أين تنظر.
- مجموعات بيانات فارغة. حلقة على صفر صفوف تنتج صفحة فيها رأس ولا شيء تحته، وهذا على الأقل يبدو مقصودًا. أما صفحة فارغة بلا رأس فتبدو كفشل. حدد ما الذي تريده قبل النشر.
- الصف الذي يقع بالضبط على الحد. أنشئ تقريرًا يكون آخر صف فيه أعلى الهامش بخطوة واحدة، ثم تقريرًا يكون الصف التالي فيه أسفل منه بخطوة واحدة. خطأ خطوة واحدة في التقسيم إلى صفحات يظل مخفيًا حتى تكون البيانات بالطول الخطأ تمامًا.
- القيم الأطول من اللازم. اسم شركة أعرض من عموده سيتمدّد إلى العمود التالي. قِس الحقل وحدد سياسة: لفّه إلى سطر ثانٍ، أو اقصه، أو اقتطعه بعلامة حذف. الصمت ليس سياسة.
- الحقول الخالية. قراءة قيمة null مباشرة إلى
TextOutقد تظهر كنص حرفيNullأو كفراغ، تبعًا لكيفية تحويلها. اختر العرض عمدًا بدل أن تدع تحويلVariantيختار نيابةً عنك.
شغّل الناتج عبر أكثر من عارض قبل أن تعتبره منتهيًا. سلوك استبدال الخطوط والقص يختلف بين العارضين، ويمكن لجدول يبدو صحيحًا في قارئ PDF واحد أن يُظهر عمودًا غير محاذٍ أو مدينة مقصوصة في قارئ آخر. تأكد من أن الرأس المتكرر وتظليل الصفوف والهوامش تبقى ثابتة أثناء النقل، وأن أرقام الصفحات تظل متسلسلة بعد أن تعبر البيانات الحد.
رسم الشبكة بنفسك بدلًا من الاتكال على مصمم تقارير مرئي يعني مزيدًا من الكود، والمقايضة هنا تستحق أن تُقال بصراحة: أنت تملك كل إحداثي، وهذا بالضبط ما تريده لمهام المعالجة الدفعية على الخادم، والفواتير، وتصديرات التدقيق التي يجب أن تُعرض بالشكل نفسه على كل آلة، وهو أيضًا العبء الذي تفضّل تجنبه في قائمة داخلية لمرة واحدة. بالنسبة إلى النوع الأول، يدفع هذا التحكم ثمنه من أول مرة يجب أن يبدو فيها التقرير في الإنتاج كما بدا على مكتبك.
تعتمد الخطوط والشرائط المظللة أعلاه على نفس بدائيات الرسم المتجهي واللون التي يغطيها شرح الرسم على اللوحة إذا أردت التعامل مع استدعاءات Rectangle وMoveTo وLineTo كل على حدة أولًا. عناصر الرسم المستخدمة هنا جزء من HotPDF Component لـ Delphi و C++Builder.