บทความเทคนิค

สร้างและปลดปล่อยคอมโพเนนต์ HotPDF แบบไดนามิกใน C++Builder

การลาก THotPDF ไปวางบนฟอร์มขณะ design time นั้นเหมาะสำหรับการทำตัวต้นแบบอย่างรวดเร็ว แต่นั่นจะเป็นการผูกอายุการใช้งานของคอมโพเนนต์เข้ากับฟอร์ม ซึ่งโค้ดในระดับ production มักไม่ต้องการแบบนั้น ตัวสร้างรายงานที่ทำงานเพียงครั้งเดียวเมื่อคลิกปุ่ม, เธรดเซอร์วิสที่รวบรวมการส่งออกข้ามคืน, คลาสช่วยเหลือที่ไม่มีฟอร์มเลย: ในแต่ละสถานการณ์เหล่านี้ คุณต้องการให้คอมโพเนนต์มีอยู่ตามระยะเวลาของงาน PDF หนึ่งงานแล้วก็หายไป นั่นหมายถึงการจัดสรรหน่วยความจำขณะ runtime ซึ่งทำให้มีสองสิ่งที่ควรทำความเข้าใจก่อนเริ่มเขียนโค้ดบรรทัดแรก: ใครคือเจ้าของอ็อบเจ็กต์ และกระบวนการทำความสะอาดทำงานอย่างไรเมื่อมีบางอย่างผิดพลาด

ความหมายของ Owner ใน VCL

คอนสตรักเตอร์ของคอมโพเนนต์ VCL ทุกตัวจะรับพารามิเตอร์ Owner ที่เป็นประเภท TComponent* การส่ง this (ตัวฟอร์มเอง) จะลงทะเบียนอ็อบเจ็กต์ใหม่เข้ากับรายการคอมโพเนนต์ที่ฟอร์มเป็นเจ้าของ ดังนั้นหากฟอร์มถูกทำลายในขณะที่คอมโพเนนต์ยังมีชีวิตอยู่ VCL จะจัดการปลดปล่อยมันให้โดยอัตโนมัติ การส่ง nullptr หมายถึงไม่มีเจ้าของ: คุณจะต้องรับผิดชอบต่อพอยน์เตอร์นั้นแต่เพียงผู้เดียว และจะไม่มีอะไรมาทำความสะอาดให้คุณหากมี exception เกิดขึ้นจนทำให้สแต็กถูกคลายออก (unwind) ก่อนที่คุณจะเรียก delete อย่างชัดแจ้ง

สำหรับการส่งออกครั้งเดียวจบที่ทำเสร็จภายในฟังก์ชันเดียว ทั้งสองวิธีนี้สามารถใช้งานได้ แต่จะมีโหมดความล้มเหลว (failure modes) ที่แตกต่างกัน การใช้ this เป็นเจ้าของ โอกาสที่หน่วยความจำจะรั่วไหลนั้นเป็นไปไม่ได้ตราบใดที่ฟอร์มนั้นจะถูกปิดในท้ายที่สุด แต่ถ้าใช้ nullptr พอยน์เตอร์จะต้องไปถึงบล็อก __finally ในทางปฏิบัติแล้ว รูปแบบ nullptr ร่วมกับ __finally จะดูสะอาดตากว่าเล็กน้อยสำหรับอ็อบเจ็กต์ที่มีอายุสั้น เนื่องจากมันทำให้ขอบเขตของอายุการใช้งานชัดเจนเพียงแค่กวาดตามอง และช่วยหลีกเลี่ยงไม่ให้ฟอร์มสะสมอ็อบเจ็กต์ที่มีไว้ใช้เพียงชั่วคราว

โครงสร้างที่ปลอดภัยต่อ Exception

การสร้าง PDF อาจล้มเหลวได้จากเหตุผลที่ไม่ได้เกี่ยวข้องกับ API เลย: ไดเรกทอรีสำหรับส่งออกอ่านได้อย่างเดียว (read-only), ไฟล์ฟอนต์หายไป, สตรีมถูกแฟลชก่อนกำหนด หรือข้อมูลที่ผู้เรียกจัดหาให้มีขนาดยาวเกินขีดจำกัด ไม่ว่าสาเหตุจะเป็นอะไร กระบวนการทำความสะอาด (cleanup) จะต้องทำงานเสมอ วิธีการที่เป็นสำนวนเฉพาะ (idiomatic) ใน C++Builder สำหรับการรับประกันสิ่งนั้นคือ try/__finally:

#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
#pragma link "HPDFDoc"
#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    THotPDF* Pdf = new THotPDF(nullptr);
    try
    {
        Pdf->FileName = "output.pdf";
        Pdf->Compression = cmFlateDecode;
        Pdf->FontEmbedding = true;
        Pdf->BeginDoc();
        Pdf->CurrentPage->SetFont("Arial", TFontStyles(), 12);
        Pdf->CurrentPage->TextOut(72, 720, 0, L"Hello from C++Builder");
        Pdf->EndDoc();
    }
    __finally
    {
        delete Pdf;
    }
}

มีบางสิ่งที่ควรกล่าวถึงในโค้ดตัวอย่างนั้น การกำหนดเจ้าของเป็น nullptr ทำให้ระบุอายุการใช้งานได้อย่างชัดแจ้ง การกำหนด Compression และ FontEmbedding จะถูกตั้งค่าก่อนเรียก BeginDoc: ทั้งสองอย่างนี้เป็นตัวเลือกในระดับเอกสารที่ HotPDF จะคอมมิตเมื่อเอกสารเปิดขึ้นมา และการกำหนดค่าหลังจากนั้นจะไม่มีผลใด ๆ เมธอด TextOut จะรับพิกัดในหน่วยพอยต์ (points) ซึ่งวัดจากมุมล่างซ้ายของหน้าเอกสาร โดยค่า Y จะเพิ่มขึ้นเมื่อเลื่อนขึ้นด้านบน ส่วนตัวเลข 72, 720 จะวางข้อความไว้ใกล้กับมุมซ้ายบนของหน้ากระดาษขนาด letter โดยมีระยะขอบซ้าย 1 นิ้ว คำสั่ง delete Pdf ในบล็อก __finally จะทำงานเสมอ ไม่ว่า BeginDoc, การวาด หรือ EndDoc จะเกิด exception หรือไม่ก็ตาม

หลีกเลี่ยงการเรียกใช้เมธอดใด ๆ บน Pdf หลังจาก delete ไปแล้ว หากพอยน์เตอร์ถูกจัดเก็บไว้ในตัวแปรคลาส ให้ตั้งค่าเป็น nullptr ทันทีหลังจากการลบ เพื่อที่ว่าหากมีการเข้าถึงโดยบังเอิญในภายหลัง มันจะทำให้โปรแกรมแครชอย่างชัดเจนแทนที่จะเกิดการพังแบบเงียบ ๆ (silent corruption)

การกำหนดค่าโปรเจ็กต์ (Project configuration)

C++Builder จะค้นหา THotPDF ผ่านการใช้เส้นทาง include (include paths), เส้นทาง library (library paths) และคำสั่ง pragma ร่วมกัน ไฟล์เฮดเดอร์ที่ถูกสร้างขึ้นจะอยู่คู่กับ HPDFDoc.pas ในไดเรกทอรีซอร์สของ HotPDF; ให้เพิ่มไดเรกทอรีนั้นไปที่ Project > Options > C++ Compiler > Include path คำสั่ง #pragma link "HPDFDoc" จะบอกให้ linker ดึง unit ที่คอมไพล์แล้วเข้ามาโดยไม่ต้องใส่ไว้ในไฟล์โปรเจ็กต์เองแบบแมนนวล หากคุณกำลังใช้งานแพ็กเกจ runtime แทนการเชื่อมโยงแบบ static (static linking) ให้ติดตั้งแพ็กเกจ design และ runtime ของ HotPDF เสียก่อน; ซึ่งคำสั่ง pragma ก็ยังคงใช้งานได้ตามปกติ

ควรเก็บชื่อ unit HPDFDoc ไว้ตามเดิม C++Builder จะอ้างอิงชื่อไฟล์เฮดเดอร์จากชื่อ unit ของ Pascal ดังนั้นการเปลี่ยนชื่อไฟล์หรือใช้นามแฝงของเส้นทาง (path alias) ในคำสั่ง pragma จะทำให้การค้นหาพังลงอย่างเงียบ ๆ

ขอบเขต (Scoping) และงานที่มีหลายเอกสาร

สำหรับการส่งออกเพียงครั้งเดียวที่ถูกทริกเกอร์จากการกระทำของผู้ใช้ ตัวแปรแบบ local ที่จำกัดขอบเขต (scoped) ให้อยู่ในฟังก์ชันของปุ่มคือคำตอบที่ถูกต้อง: มันจะถูกสร้าง ใช้งาน และถูกทำลายภายในหนึ่ง call frame และเจตนานั้นก็ชัดเจนสำหรับทุกคนที่มาอ่านโค้ดในภายหลัง ทางเลือกแบบ design-time นั้นมีเหตุผลรองรับเมื่อฟอร์มเดียวกันนี้ต้องขับเคลื่อนเวิร์กโฟลว์อย่างต่อเนื่อง เช่น พาเนลพรีวิวการพิมพ์ที่จะสร้างเอกสารขึ้นใหม่ทุกครั้งที่ผู้ใช้เปลี่ยนการตั้งค่า; ในกรณีนั้น การรักษาคอมโพเนนต์ให้มีชีวิตอยู่แล้วเรียกใช้ BeginDoc/EndDoc ซ้ำ ๆ ย่อมรบกวนระบบน้อยกว่าการจัดสรรและปลดปล่อยอ็อบเจ็กต์บนฮีป (heap) ซ้ำไปซ้ำมา

สำหรับงานแบบชุด (batch jobs) ที่สร้างเอกสารหลายฉบับเรียงตามลำดับ การจำกัดขอบเขตของ THotPDF หนึ่งตัวต่อเอกสารหนึ่งฉบับก็คุ้มค่ากับโอเวอร์เฮดของการจัดสรรหน่วยความจำ สถานะ (State) จะไม่ถูกส่งต่อไปมาระหว่างเอกสารหากไม่มีอ็อบเจ็กต์มารองรับสถานะนั้น และนั่นถือเป็นหนึ่งในรูปแบบของบั๊กเป็นๆ หายๆ ที่คุณจะไม่มีวันต้องไปดีบักมัน ให้ใช้รูปแบบ จัดสรรหน่วยความจำ, สร้างเอกสาร, ลบทิ้ง แล้วทำซ้ำ

หนึ่งในคุณสมบัติที่ปรากฏในเดโมของ HotPDF หลายอันคือ AutoLaunch ซึ่งจะเปิดไฟล์ที่สร้างขึ้นมาในโปรแกรมดู PDF ของระบบทันทีหลังจากเรียก EndDoc คุณสมบัตินี้มีประโยชน์ขณะกำลังเขียนแบบร่างแรกของเค้าโครง (layout) แต่ในระดับ production ขอแนะนำให้ข้ามการใช้งานไป: เปิดเส้นทางส่งออก (output path) อย่างชัดเจน, ยืนยันว่าไฟล์นั้นมีอยู่และมีขนาดไม่เป็นศูนย์, บันทึกผลลัพธ์การทำงาน (log) แล้วปล่อยให้เวิร์กโฟลว์ที่เรียกใช้งานเป็นผู้ตัดสินใจว่าการเปิดโปรแกรมดูเอกสารนั้นมีความเกี่ยวข้องหรือไม่ สำหรับในงานแบบแบตช์ AutoLaunch จะเปิดหน้าต่างโปรแกรมดูเอกสารหนึ่งหน้าต่างต่อหนึ่งเอกสาร และจะบล็อกการทำงาน (block the process) ในบางระบบเพื่อรอให้โปรแกรมดูเอกสารนั้นปิดลง

คอมโพเนนต์ THotPDF และการเรียกใช้คำสั่งการวาดทั้งหมดที่แสดงในที่นี้ เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder