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

การสร้างรายงาน Excel ตามเทมเพลตใน Delphi ด้วย HotXLS

วิธีที่เชื่อถือได้ในการสร้างรายงาน Excel ที่มีสไตล์สวยงามจาก Delphi คือเริ่มจาก workbook ที่ designer สร้างไว้แล้ว ใครสักคนในฝ่ายการเงินจัดวางใบแจ้งหนี้ใน Excel: โลโก้ หัวคอลัมน์ เส้นขอบของแถบรายละเอียด แถวยอดรวมตัวหนา รูปแบบสกุลเงิน โค้ดของคุณเปิดไฟล์นั้น เติมข้อมูลจริงลงในเซลล์ที่ designer เตรียมไว้ให้ แล้ว save ผลลัพธ์ หน้าตาเป็นของพวกเขา ตัวเลขเป็นของคุณ HotXLS ไลบรารี native สำหรับ Delphi และ C++Builder ที่อ่านและเขียน workbook แบบ XLS และ XLSX ได้โดยไม่ต้องขับเคลื่อน Excel มอบสามการดำเนินการที่แนวทางนี้ต้องการให้กับคุณ: ค้นหาเซลล์ด้วยข้อความของมัน คัดลอกช่วงพร้อมสไตล์และสูตรให้คงเดิม และแทรกแถวเพื่อให้ทุกอย่างข้างล่างเลื่อนลงตามข้อมูล

กฎข้อเดียวที่แยกตัวสร้างรายงานที่รอดจากการแก้ไข template ออกจากตัวที่พังตั้งแต่ครั้งแรกคือ ห้ามอ้างอิงเซลล์ด้วยเลขแถวและคอลัมน์แบบตายตัวเด็ดขาด template คือเอกสารที่คนอื่นแก้ไขได้ ทีมการเงินเพิ่มบรรทัดภาษี ขยายความสูงของแถวโลโก้ จัดเรียงบล็อกที่อยู่ใหม่ และรูปแบบไฟล์ก็ไม่ช่วยคุณเลยสักนิด: การ save แบบ BIFF หรือ OOXML สำเร็จเสมอไม่ว่าแถวที่ 10 จะยังมีความหมายเหมือนไตรมาสที่แล้วหรือไม่ ตัวสร้างรายงานที่เขียนบรรทัดรายละเอียดแรกลงแถวที่ 10 แบบตายตัว จะทำสิ่งนี้ในครั้งแรกที่มีใครแทรกบล็อกไว้เหนือส่วนรายละเอียด: มันจะประทับรายการสินค้าทับเซลล์ผิด และรวมยอดในช่วงที่ไม่ครอบคลุมข้อมูลอีกต่อไป ไม่มีอะไร throw เลย ทุกการ save คืนค่าสำเร็จ และสัญญาณเดียวที่มีคือลูกค้าสังเกตเห็นใบแจ้งหนี้ที่ผิดพลาด

แผนภาพไปป์ไลน์เทมเพลตของ HotXLS ใน Delphi: ยึด token ด้วย FindText, ขยายแถบรายละเอียด, ตรวจยอดรวมที่คำนวณแล้ว จากนั้นบันทึก
การสร้างรายงานจากเทมเพลตใน Delphi ทำงานเป็นสี่เฟสของ HotXLS: ยึดจุดโทเคน ขยายแถบรายละเอียด ตรวจสอบผลรวมที่คำนวณได้ แล้วจึงส่งมอบ

ผูกทุกพิกัดไว้กับ placeholder token

ทางแก้คือทำให้ template พกพิกัดของตัวเองไว้ designer เขียน token อย่าง {{CUSTOMER}}, {{DATE}} และ {{DETAIL_START}} ลงในเซลล์ที่ตัวสร้างรายงานต้องแตะ แล้วตัวสร้างรายงานก็คำนวณหาตำแหน่งทุกจุดตอนรันไทม์จากที่มันเจอ token เหล่านั้น การแก้ layout ไม่สำคัญอีกต่อไป เพราะ token เคลื่อนตามเซลล์ที่มันอยู่เสมอ ครึ่งหลังของ contract นี้คือกฎความล้มเหลว: ถ้า token ที่จำเป็นหายไป งานต้องหยุดก่อนที่ข้อมูลลูกค้าใด ๆ จะไปถึงไฟล์ template ที่เบี้ยวไปแล้วควรสร้าง job ticket ที่ล้มเหลว ไม่ใช่เอกสารที่ส่งมอบออกไป

ค้นหา token: FindText และ ReplaceText

class family ทั้งสองตระกูลของ HotXLS เปิดให้ค้นหาระดับ worksheet ได้ FindText คืนแถวและคอลัมน์ของเซลล์แรกที่ข้อความตรงกัน พร้อม overload ที่เพิ่มการตรวจสอบตัวพิมพ์เล็กใหญ่ ReplaceText สลับทุกจุดที่พบและคืนจำนวนที่เปลี่ยนกลับมา สองเมธอดนี้ครอบคลุม token สองแบบที่คุณมักเจอ แบบแรกคือจุดยึดเดี่ยวอย่างชื่อลูกค้าที่คุณค้นหาครั้งเดียวแล้วเขียนต่อจากมัน แบบที่สองคือ token ที่ควรปรากฏพอดีหนึ่งครั้ง อย่างวันที่ของรายงาน ที่คุณแทนที่แล้วตรวจสอบจำนวน ในฝั่ง XLSX การเติมข้อมูลที่ยึดตัวเองแบบนี้จะหน้าตาแบบนี้:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R, C: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('invoice-template.xlsx') <> 1 then
      raise Exception.Create('Cannot open invoice template');
    Sheet := Book.Sheets[0];               // TXLSXSheets.Items เริ่มที่ 0

    if not Sheet.FindText('{{CUSTOMER}}', R, C) then
      raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
    Sheet.Cells[R, C].Value := 'ACME Corp';

    if Sheet.ReplaceText('{{DATE}}',
         FormatDateTime('yyyy-mm-dd', Date)) = 0 then
      raise Exception.Create('Template drift: {{DATE}} token missing');
    // การขยายรายละเอียดและการ save ตามมาข้างล่าง
  finally
    Book.Free;
  end;
end;

มีรายละเอียดสองอย่างที่สำคัญ อย่างแรกคือ FindText และ ReplaceText จับคู่กับค่าข้อความของเซลล์เท่านั้น token ที่ฝังอยู่ในสตริงของสูตรจะมองไม่เห็นเลยสำหรับทั้งสองเมธอดนี้ ดังนั้น placeholder token ต้องอยู่ในเซลล์ธรรมดาเท่านั้น ไม่ใช่ในสูตร อย่างที่สองคือจำนวนการแทนที่คือตัวตรวจจับความเบี้ยวของคุณ template ที่ควรมี token {{DATE}} พอดีหนึ่งตัว แต่รายงานว่าแทนที่ไปศูนย์ครั้ง ก็แปลว่าถูกแก้ไขไปแล้ว และการโยน exception ในจังหวะนั้นแหละคือสิ่งที่เปลี่ยนความเบี้ยวของ layout แบบเงียบ ๆ ให้กลายเป็นความล้มเหลวที่มองเห็นได้

โคลนแถวรายละเอียดโดยไม่สูญเสียสไตล์หรือสูตร

ส่วนรายละเอียดของใบแจ้งหนี้ขยายตามข้อมูล การเขียนค่าตรงลงในแถวว่างใต้บรรทัดตัวอย่างจะทิ้งทุกอย่างที่ designer เตรียมไว้ไปหมด: เส้นขอบ รูปแบบตัวเลข สูตรรายแถว แพทเทิร์นที่รักษาทุกอย่างเหล่านั้นไว้คือเหลือแถวตัวอย่างที่จัดรูปแบบครบไว้หนึ่งแถวใน template แล้วโคลนมันสำหรับแต่ละรายการ CopyRange ทำสำเนาสไตล์และสูตรในการเรียกครั้งเดียว จากนั้นตัวสร้างรายงานก็เขียนทับแค่เซลล์ค่าเท่านั้น

แผนภาพ anchor token ในเทมเพลต HotXLS บน Delphi ที่ placeholder ที่หายไปทำให้งานตก ก่อนมีข้อมูลใดถูกเขียนเลย
โทเคนเทมเพลตพาพิกัดของตัวเองมาด้วย และโทเคนที่หายไปจะหยุดงานก่อนข้อมูลใด ๆ ถูกเขียน
const
  DetailRow = 10;            // แถวตัวอย่างที่จัดรูปแบบไว้ใน template
var
  I: Integer;
begin
  // เปิดพื้นที่ก่อนบล็อกยอดรวมก่อน เพื่อให้ช่วง SUM
  // ใต้แถบรายละเอียดขยายไปพร้อมกับข้อมูล
  if Length(Items) > 1 then
    Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);

  for I := 0 to High(Items) do
  begin
    if I > 0 then              // โคลนสไตล์ + สูตรจากแถวตัวอย่าง
      Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
    Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
    Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
    Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
    Sheet.Cells[DetailRow + I, 4].Formula :=
      Format('B%d*C%d', [DetailRow + I, DetailRow + I]);  // ไม่มีเครื่องหมาย '=' นำหน้า
  end;
end;

สังเกตการกำหนดค่าสูตรให้ดี property Formula ของ XLSX รับ expression โดยไม่มีเครื่องหมายเท่ากับนำหน้า ในขณะที่ facade ของ XLS คาดหวัง '=B10*C10' ที่กำหนดผ่าน Value การผสมสองธรรมเนียมนี้เข้าด้วยกันคือความผิดพลาดที่พบบ่อยที่สุดตอนย้ายโค้ดระหว่าง class family ทั้งสองตระกูล และมันล้มเหลวแบบไม่มีคำเตือนใด ๆ: เซลล์จะแค่เก็บสตริงตัวอักษรที่ Excel แสดงเป็นข้อความธรรมดา ถ้า template ตกแต่งแถบรายละเอียดด้วยแถวหัวข้อที่ merge ไว้ ให้จำไว้ว่ามีแค่เซลล์บนซ้ายสุดของพื้นที่ merge เท่านั้นที่เก็บค่าไว้ กฎ layout ใน บทความคู่กันเรื่อง merged cells ใน layout-driven report template อธิบายว่าทำไมพื้นที่ merge จึงควรอยู่นอกแถบข้อมูลไปเลยทั้งหมด

InsertRows ย้ายอะไร และทิ้งอะไรไว้ข้างหลัง

การแทรกแถวไว้ก่อนบล็อกยอดรวมคือสิ่งที่ทำให้ช่วง SUM ขยายตัวตามส่วนรายละเอียดที่โตขึ้น ในฝั่ง XLSX นั้น InsertRows พาโครงสร้างที่พึ่งพากันยาวเหยียดเลื่อนลงไปพร้อมเซลล์ด้วย: ช่วงที่ merge ไว้ ความสูงแถว hyperlink comment frozen pane ช่วง autofilter conditional format data validation ตาราง defined name และจุดยึดของรูปภาพและแผนภูมิ มีขอบเขตหนึ่งในรายการนั้นที่ควรจำไว้ การเขียนสูตรใหม่เอื้อมไปถึงแค่การอ้างอิงภายใน sheet เดียวกันเท่านั้น สูตรบน sheet สรุปที่ชี้เข้าไปในพื้นที่ที่ถูกย้าย จะยังคงพิกัดเดิมไว้และแอบอ่านเซลล์ผิดไปแบบเงียบ ๆ นี่คือเหตุผลที่ยอดรวมที่ดึงข้ามชีทควรแสดงผ่าน workbook-level name จะปลอดภัยกว่า บทความคู่กันเรื่อง defined names และสูตรข้ามชีท อธิบายแพทเทิร์นนั้นไว้อย่างละเอียด

ฟอร์แมต XLS แบบดั้งเดิมขีดเส้นไว้ที่จุดที่แข็งกว่า HotXLS เก็บ pivot table, query table และการเชื่อมต่อข้อมูลภายนอกในไฟล์ BIFF ไว้เป็นก้อนไบต์ดิบ มันรอดจากการเปิดและ save โดยไม่เปลี่ยนแปลง แต่มันไม่ได้ถูกโมเดลไว้ ดังนั้นการแทรกแถวจึงไม่แตะมันเลย template ที่จอด pivot table ไว้ใต้บล็อกรายละเอียดที่กำลังขยายตัวจะ save ได้โดยไม่มีคำเตือนใด ๆ เลย ในขณะที่สี่เหลี่ยมต้นทางของ pivot ก็เบี่ยงออกจากข้อมูลไปเรื่อย ๆ ทางออกคือแก้ที่โครงสร้าง ไม่ใช่การป้องกันเฉพาะหน้า: เก็บเนื้อหา pivot และ query ไว้บน sheet ที่ตัวสร้างรายงานไม่เคยแทรกแถวเข้าไปเลย แล้วความล้าสมัยแบบนี้ก็จะไม่มีทางเกิดขึ้น

แผนภาพว่า InsertRows ของ HotXLS ย้ายอะไรใน XLSX และขอบเขตสูตรข้ามชีตกับ pivot BIFF ที่ตัวสร้าง Delphi ต้องเคารพ
InsertRows พาโครงสร้างที่พึ่งพากันลงไปบน XLSX ในขณะที่สูตรข้ามชีตและบล็อกดิบของ BIFF ทำเครื่องหมายเขตแดน

คำนวณใหม่ก่อนส่งมอบ หรือรู้เหตุผลว่าทำไมถึงข้ามขั้นตอนนี้ไป

HotXLS ไม่ประเมินค่าสูตรระหว่าง SaveAs เมื่อคนเปิดไฟล์ Excel จะคำนวณทุกอย่างใหม่ให้ (facade ของ XLS เปิดให้ใช้ CalculationMode และ RecalcOnSave ถ้าคุณต้องการควบคุมเรื่องนั้น) ดังนั้นรายงานที่มุ่งไปยังกล่องขาเข้าของมนุษย์ไม่ต้องการอะไรเพิ่มจากคุณอีกเลย ภาพจะเปลี่ยนไปทันทีที่ workbook ป้อนข้อมูลให้โปรแกรมอื่น การ export CSV เขียนสูตรออกมาเป็นข้อความตัวอักษรล้วน ๆ และไม่เคยคำนวณมันเลย และ parser ปลายทางใดก็ตามที่เชื่อค่าที่แคชไว้ก็จะอ่านตัวเลขเก่าหรือค่าว่างไปแทน สำหรับเส้นทางเหล่านั้น ให้คำนวณบนเซิร์ฟเวอร์ด้วย Calculate ซึ่งประเมินค่า expression ใด ๆ ก็ได้กับ workbook ที่โหลดอยู่แล้วส่งผลลัพธ์กลับมา:

var
  Total: Variant;
  LastDetail: Integer;
begin
  LastDetail := DetailRow + Length(Items) - 1;
  Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
    [DetailRow, LastDetail]));
  if (not VarIsNumeric(Total)) or
     (Abs(Total - ExpectedTotal) > 0.005) then
    raise Exception.Create('Invoice total does not match the order record');

  if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
    raise Exception.Create('Save failed: check output path and permissions');
end;

การตรวจสอบยอดรวมที่คำนวณได้กับข้อมูลคำสั่งซื้อก่อน save คือประกันราคาถูกที่คุ้มค่ามาก มันเปลี่ยนใบแจ้งหนี้ที่ผิดพลาดให้กลายเป็นงานที่ล้มเหลวแทน operator ลองงานที่ล้มเหลวใหม่ได้ในไม่กี่วินาที ส่วนใบแจ้งหนี้ที่ผิดพลาดที่ไปอยู่ในกล่องจดหมายของลูกค้าแล้วต้องแลกด้วยคำขอโทษและการแก้ไขจาก account manager

class family สองตระกูล อัลกอริทึมเดียว

ตรรกะเดียวกันย้ายข้ามฟอร์แมตได้ แต่โค้ดไม่เหมือนกัน TXLSWorkbook สำหรับ .xls แบบดั้งเดิมเป็นแบบ interface-based และนับ reference โดยมี sheet index เริ่มที่ 1 และคุณไม่ต้อง free มันด้วยมือเลย TXLSXWorkbook สำหรับ .xlsx เป็นอ็อบเจ็กต์ธรรมดาที่คุณต้อง free เองใน try..finally โดยมี sheet index เริ่มที่ 0 และธรรมเนียมสูตรตามที่แสดงไว้ข้างต้น FindText, ReplaceText, CopyRange และ InsertRows อยู่ในทั้งสองฝั่ง ดังนั้นรูปแบบผูก-โคลน-คำนวณใหม่จึงย้ายข้ามได้อย่างราบรื่น คำแนะนำเชิงปฏิบัติคือให้ยึดฟอร์แมตเดียวต่อหนึ่ง pipeline หรือไม่ก็ซ่อน object lifecycle ทั้งสองแบบไว้หลัง adapter บาง ๆ ของคุณเอง แทนที่จะปล่อยให้ความต่างกระจายอยู่ทั่วตัวสร้างรายงาน

ขนาดแทบไม่สำคัญเลยสำหรับรายงานประเภทที่แพทเทิร์นนี้สร้างขึ้น การโคลนแถวที่มีสไตล์ซ้ำ ๆ หลายพันครั้งไม่ใช่เรื่องใหญ่สำหรับฮาร์ดแวร์ปัจจุบันเลย save path จะกลายเป็นคอขวดก็ต่อเมื่อแถบรายละเอียดวิ่งไปถึงหลักแสนแถวเท่านั้น และตอนนั้นการตั้งค่า StreamingWrite จะส่ง XML ของ worksheet ตรงเข้าไปในแพ็กเกจผลลัพธ์แทนที่จะบัฟเฟอร์มันไว้ บทความเรื่อง streaming write สำหรับงาน batch บนเซิร์ฟเวอร์ ครอบคลุมว่าเมื่อไรการแลกเปลี่ยนนั้นถึงคุ้มค่า แผนภูมิก็มีพฤติกรรมแบบเดียวกับส่วนที่เหลือของ layout: ในฝั่ง XLSX ทั้งจุดยึดของแผนภูมิและการอ้างอิง series ของมันเลื่อนตามเมื่อ InsertRows ทำงานอยู่เหนือมัน ดังนั้นแผนภูมิใต้แถวยอดรวมจึงยังผูกกับข้อมูลที่ถูกต้องเสมอ ในขณะที่ฝั่ง XLS แผนภูมิอยู่บน chart sheet ของตัวเอง และเหมือนกับ pivot table ตรงที่ไม่เคยเลื่อนเลย นั่นคือเหตุผลเพิ่มเติมอีกข้อที่ควรเก็บ sheet นำเสนอให้ห่างจาก sheet ที่ตัวสร้างรายงานขยายอยู่

แนวทางผูก-โคลน-คำนวณใหม่นี้ทำให้ designer เป็นเจ้าของว่า workbook หน้าตาเป็นอย่างไร ในขณะที่โค้ดของคุณเป็นเจ้าของว่ามันพูดอะไร ซึ่งโดยทั่วไปแล้วเป็นสิ่งที่ทำให้ผลลัพธ์ Excel ที่สร้างขึ้นมาคุ้มค่าต่อการดูแลรักษา เมธอดค้นหา คัดลอก และแทรกที่แสดงไว้ในบทความนี้ พร้อมกับเอนจิ้นสูตรที่ใช้ตรวจสอบยอดรวมก่อนส่งมอบ มาพร้อมกับ HotXLS Delphi Component สำหรับ Delphi และ C++Builder