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

การเปิดและบันทึกสเปรดชีต ODS ใน Delphi ด้วย HotXLS

ระบบหลังบ้านของรายงาน Delphi ที่ปล่อย .xlsx มาหลายปีแล้ว รับข้อกำหนดใหม่เข้ามา: กฎการจัดซื้อจัดจ้างของลูกค้าภาครัฐกำหนดให้ต้องส่งออกเป็น OpenDocument Spreadsheet และนักวิเคราะห์ในบัญชีนั้นก็ส่งการแก้ไขกลับมาเป็นไฟล์ .ods ที่ save จาก LibreOffice ตอนนี้โค้ดตัวเดียวกันจึงต้องเขียน ODS และอ่านมันด้วย HotXLS ไลบรารี spreadsheet แบบ Object Pascal ของ losLab สำหรับ Delphi และ C++Builder จัดการทั้งสองทิศทางได้โดยไม่ต้องติดตั้ง Excel หรือ LibreOffice ที่ไหนเลย สิ่งที่มันไม่ทำคือทำให้สองทิศทางนั้นสมมาตรกัน การ export พกไปมากกว่าที่การ import กู้กลับมาได้มาก และทีมที่สันนิษฐานเป็นอย่างอื่นจะเห็นสูตรและ formatting ระเหยหายไปที่ไหนสักที่ระหว่างการแก้ไขของลูกค้ากับรายงานถัดไป โดยไม่มี error ให้ชี้สาเหตุเลย

การรองรับ ODS อาศัยอยู่บนส่วนหน้า XLSX ไม่ใช่ XLS

HotXLS มาพร้อม class hierarchy อิสระสองชุดในแพ็กเกจเดียว: TXLSWorkbook ใน unit lxHandle สำหรับไฟล์ .xls แบบ binary BIFF8 และ TXLSXWorkbook ใน unit lxHandleX สำหรับ package OOXML .xlsx จุดเข้าถึง OpenDocument ทุกจุด ทั้ง OpenODS, SaveAsODS, GetODSSheetNames แขวนอยู่บน TXLSXWorkbook ทั้งหมด ตำแหน่งที่ว่านี้ไม่ได้สุ่มเลือกมา package ODS ตามที่ระบุใน OASIS ODF 1.3 คือไฟล์ zip ที่พกสมาชิก mimetype, manifest และเนื้อหา content.xml ซึ่งทำให้มันเป็นญาติเชิงโครงสร้างของ zip แบบ OOXML ส่วน BIFF8 คือ binary record stream จากยุค 1990 ที่ไม่มีอะไรร่วมกันเลย

ตำแหน่งนั้นมีข้อจำกัดเชิงปฏิบัติอยู่: workbook .xls รุ่นเก่าจะกลายเป็น .ods ในการเรียกครั้งเดียวไม่ได้ คุณต้องเชื่อมเนื้อหา BIFF เข้าสู่โมเดล XLSX ก่อนด้วย SaveXLSWorkbookAsXLSX จาก unit lxXlsxExport เปิดผลลัพธ์นั้นใหม่ผ่าน TXLSXWorkbook แล้วค่อย export จากตรงนั้น bridge นี้ไม่ได้รักษาข้อมูลครบทุกอย่าง และควรรู้ช่องว่างของมันไว้ก่อนที่คุณจะสร้างต่อยอด มันคัดลอกค่า สูตร รูปแบบตัวเลข ฟอนต์ fill และความกว้างคอลัมน์ มันทิ้งเส้นขอบ range ที่ merge ไว้ comment แผนภูมิ และ conditional formatting แหล่งข้อมูล .xls ที่มี formatting หนัก ๆ จะไปถึง ODS ในสภาพที่ดูเรียบกว่าตอนที่มันจากมา และนั่นคือคุณสมบัติของ bridge ไม่ใช่ของตัวเขียน ODS

การตรวจจับฝั่ง import เป็นแบบอัตโนมัติ เมธอด Open ธรรมดาจดจำ package ODS ได้จากสมาชิก mimetype ของมัน แล้วถอยไปตรวจ content.xml ระดับบนสุดเมื่อสมาชิกนั้นไม่มีอยู่ ดังนั้น code path แบบทั่วไปที่ "เปิดอะไรก็ตามที่ผู้ใช้อัปโหลดมา" จึงไม่ต้องดมกลิ่นนามสกุลไฟล์ของตัวเองเลย หลังจากเปิดแล้ว property SourceFormat จะรายงานว่าสาขาไหนที่ทำงาน

แผนภาพผังคลาสของ HotXLS ใน Delphi ที่จุดเข้า ODS ทุกจุดอยู่บน TXLSXWorkbook และสะพาน SaveXLSWorkbookAsXLSX พาเนื้อหา .xls แบบ BIFF8 ข้ามไป
จุดเข้าถึง OpenDocument ทุกจุดแขวนอยู่กับ TXLSXWorkbook และไฟล์ .xls รุ่นเก่าไปถึง ODS ได้ผ่านสะพาน BIFF เป็น XLSX ที่สูญเสียข้อมูลเท่านั้น

การ export ไปยัง ODS ด้วย TODSExportOptions

call สำหรับ export เองเป็นแค่บรรทัดเดียว แต่ object ตัวเลือกที่ล้อมรอบมันพกการตัดสินใจที่ผู้ตรวจสอบจะถามถึงในภายหลัง:

var
  Book: TXLSXWorkbook;
  Opts: TODSExportOptions;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    Opts := TODSExportOptions.Create;        // ผู้เรียกเป็นเจ้าของและต้อง free มันเอง
    try
      Opts.Generator := 'ReportService 4.2'; // override ค่า meta:generator
      Opts.IncludeCharts := True;
      Opts.IncludeImages := True;
      Book.SaveAsODS('quarterly-report.ods', Opts);
    finally
      Opts.Free;
    end;
  finally
    Book.Free;
  end;
end;

object ตัวเลือกเป็นของผู้เรียก HotXLS จะไม่ free มันให้ ซึ่งเป็นเหตุผลว่าทำไม try..finally ชั้นในถึงอยู่ตรงนั้นและไม่ใช่ตัวเลือก property สองตัวที่เปลี่ยน output จริง ๆ แทนที่จะแค่ติดป้ายมัน คุ้มค่าที่จะมองใกล้ ๆ การตั้งค่า IncludeCharts := False ทำมากกว่าแค่ซ่อนแผนภูมิ: มันลอกเอกสารย่อยของแผนภูมิและรายการ manifest ของมันออกจาก package เลย ซึ่งเป็นสิ่งที่คุณต้องการพอดีเมื่อผู้บริโภคคือ data pipeline ที่จะสะดุดกับมัน Generator override สตริง meta:generator ของ ODF ซึ่งปกติจะอ่านว่า HotXLS/<version> override มันเมื่อเครื่องมือปลายทางระบุลายนิ้วมือผู้ผลิตไฟล์เพื่อจัดเส้นทาง support ถ้าไม่มีข้อไหนตรงกับกรณีของคุณ ให้ข้าม options object ไปเลย การเรียก SaveAs(FileName, xlsxOpenDocumentSpreadsheet) เหมือนกับ SaveAsODS ด้วยค่าเริ่มต้นทุกประการ และ overload แบบ stream บนทั้งสองตัวให้คุณเขียน package ตรงเข้า HTTP response ได้โดยไม่ต้องมีไฟล์ชั่วคราวเลย

สิ่งที่เส้นทาง import อ่าน และสิ่งที่มันข้ามไปอย่างตั้งใจ

อ่านส่วนนี้อย่างละเอียดก่อนที่คุณจะสัญญาความเที่ยงตรงของ round-trip ให้ใครก็ตาม การ import ODS ใน HotXLS เป็นเส้นทางแบบเบา ๆ อย่างตั้งใจ มันรักษาค่าเซลล์แบบ scalar และผลลัพธ์ที่แคชไว้ซึ่งแต่ละสูตรพกมาตอน save แล้วขยายแถวและคอลัมน์ที่ทำซ้ำออกมาเป็น grid มันไม่ดึงสไตล์ นิพจน์สูตรของ ODS หรือภาพวาดข้ามมาด้วย

ทางเลือกเรื่องสูตรคือสิ่งที่มีโอกาสกัดคุณมากที่สุด และมันถูกเลือกมาโดยตั้งใจ เซลล์ของ ODF เก็บสองสิ่งไว้เคียงข้างกัน: นิพจน์สูตรที่เขียนด้วยสำเนียง OpenFormula ที่นิยามไว้ใน ODF 1.3 Part 4 และค่าล่าสุดที่แอปพลิเคชันผู้ผลิตคำนวณไว้ให้มัน การแปล OpenFormula ให้เป็น syntax สูตรของ Excel เป็นปัญหาการแปลงสำเนียงในตัวของมันเอง พร้อม edge case จริง ๆ รอบคลังคำศัพท์ฟังก์ชัน syntax ของ reference และโมเดล error การอ่านค่าที่แคชไว้แทนช่วยเลี่ยงปัญหาการแปลผิดแบบเงียบ ๆ ทั้งกลุ่มนั้นไปได้ ดังนั้นตัวเลขที่คุณ import เข้ามาจึงเป็นตัวเลขเดียวกับที่ผู้ส่งเห็นครั้งล่าสุดเป๊ะ ๆ ต้นทุนคือมันมาถึงในรูปตัวเลข ไม่ใช่สูตรที่มีชีวิตซึ่งผลิตมันขึ้นมา

รูปแบบความล้มเหลวที่ควรออกแบบให้รอบคอบตามมาโดยตรง: spreadsheet ที่ผลรวมของมันถูกต้องตอนที่ LibreOffice save มันครั้งสุดท้าย จะ import เข้ามาพร้อมตัวเลขที่ถูกต้อง แต่ตัวเลขเหล่านั้นตอนนี้กลายเป็นค่าคงที่แล้ว แก้เซลล์ input คำนวณใหม่ แล้วไม่มีอะไรขยับเลย สูตรหายไปแล้ว เหลือแค่ผลลัพธ์สุดท้ายของมัน ถ้า workflow ต้องการสูตรที่มีชีวิตหลัง import ให้สร้างมันขึ้นใหม่ด้วยโปรแกรมจากกฎธุรกิจของคุณเองผ่าน Cell.Formula ซึ่งบนส่วนหน้าของ XLSX รับนิพจน์โดยไม่มีเครื่องหมายเท่ากับนำหน้า

ออกแบบรอบ round trip ที่ไม่สมมาตร

การ export render จากโมเดล workbook เต็มรูปแบบในหน่วยความจำ: ค่า สไตล์ และถ้าคุณขอ แผนภูมิกับรูปภาพด้วย การ import คืนค่ากลับมาเฉพาะค่าเท่านั้น ดังนั้นขาที่จาก .xlsx ไป .ods จึงมีความเที่ยงตรงสูง ส่วนขาที่จาก .ods กลับมาที่ .xlsx นำค่ากับผลลัพธ์ที่แคชไว้กลับมา แต่ไม่มี styling และไม่มีสูตรที่มีชีวิต ต่อสองขานี้เข้าด้วยกันแล้วความไม่สมมาตรก็ทวีคูณขึ้น รอบ .xlsx ไป .ods แล้วกลับมา .xlsx เต็มรอบเขียนทุกอย่างอย่างซื่อสัตย์ตอนออกไป แล้วสูญเสียสไตล์และสูตรตอนกลับเข้ามา ทั้งที่ไม่มีอะไรผิดพลาดในขั้นตอนไหนเลย

แผนภาพการเดินไปกลับ ODS แบบไม่สมมาตรของ HotXLS จาก Delphi: export เต็มความเที่ยงจากโมเดลเวิร์กบุ๊กในหน่วยความจำ และ import เฉพาะค่าที่ทิ้งสูตรไว้เป็นค่าคงที่
Export เรนเดอร์โมเดลในหน่วยความจำทั้งหมด ในขณะที่ import คืนค่าและผลลัพธ์ที่แคชไว้ วงจร .xlsx เป็น .ods เป็น .xlsx เต็มรูปแบบจึงทิ้งสไตล์และสูตรที่ยังทำงานไปอย่างเงียบ ๆ
Book := TXLSXWorkbook.Create;
try
  Book.Open('vendor-revision.ods');          // ตรวจจับ format อัตโนมัติ
  if Book.SourceFormat = xlsxOpenDocumentSpreadsheet then
  begin
    // ค่าและผลลัพธ์สูตรที่แคชไว้จะมีอยู่หลังการ import ODS
    // แต่สไตล์และสูตรที่มีชีวิตจะไม่มี สร้างสิ่งที่ pipeline
    // ปลายทางต้องพึ่งพาขึ้นมาใหม่ก่อนที่จะ save
    Book.Sheets[0].Cells[2, 5].Formula := 'SUM(B2:D2)';
    Book.SaveAs('vendor-revision.xlsx');
  end;
finally
  Book.Free;
end;

รูปแบบเชิงสถาปัตยกรรมที่ตกผลึกออกมาจากนี้คือ: ปฏิบัติต่อไฟล์ .ods ที่เข้ามาเหมือนเป็น data feed ไม่ใช่เอกสารที่จะแก้ไขตรงที่มันอยู่ เก็บ workbook หลักไว้ในรูป .xlsx อ่านค่าออกมาจากการแก้ไขของลูกค้า แล้วปล่อย ODS ใหม่ตามต้องการจากสำเนาหลักนั้น การตรวจสอบควรอยู่ทั้งสองค่าย เปิดไฟล์ที่ export แล้วใน LibreOffice Calc ซึ่งเป็นผู้บริโภค ODF อ้างอิง และใน Excel ซึ่งอ่าน ODS มาหลายปีแล้วแต่ไม่ตรงกับ LibreOffice ที่ขอบเขตของการรองรับแผนภูมิและสไตล์ จำนวน sheet, เซลล์สำคัญไม่กี่ตัว และการมีอยู่ของแผนภูมิก็เพียงพอเป็น smoke check ต่อหนึ่ง export profile

คัดกรองไฟล์ ODS ก่อนที่จะเดินหน้า import

เมื่อ endpoint หนึ่งรับ upload การแสดงรายชื่อ sheet ถูกกว่าการ parse เต็มรูปแบบมาก และจับความประหลาดใจเชิงโครงสร้างได้ตั้งแต่เนิ่น ๆ:

แผนภาพประตูคัดแยกไฟล์อัปโหลดของ HotXLS ใน Delphi ที่ GetODSSheetNames ปฏิเสธแพ็กเกจ ODS ที่อ่านไม่ได้และชีตที่หายไป ก่อนการ import เต็มรูปแบบจะรัน
การสำรวจด้วย GetODSSheetNames มีต้นทุนต่ำกว่าการแยกวิเคราะห์เต็มรูปแบบมาก และจับความล้มเหลวจากชีตที่ถูกเปลี่ยนชื่อได้ในขณะที่ข้อผิดพลาดยังระบุชื่อไฟล์ได้อยู่
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetODSSheetNames('incoming.ods', Names) <= 0 then
    raise Exception.Create('not a readable ODS package');
  if Names.IndexOf('Data') < 0 then
    raise Exception.Create('revision is missing the Data sheet');
finally
  Book.Free;
  Names.Free;
end;

ธรรมเนียมค่าที่คืนกลับมักทำให้คนสะดุด: call ของ HotXLS โดยทั่วไปคืนค่าจำนวนที่เป็นบวกหรือ 1 เมื่อสำเร็จ และ -1 เมื่อล้มเหลว โดยเคลียร์รายการทิ้งเมื่อมันล้มเหลว ดังนั้นให้ทดสอบด้วย <= 0 แทนที่จะเทียบกับค่าบวกค่าใดค่าหนึ่งโดยเฉพาะ GetODSSheetNames ไม่ reset และไม่เติมข้อมูลลงใน instance ของ workbook เลย ดังนั้น object ตรวจสอบตัวเดียวจึงตรวจสอบไฟล์ที่เข้ามาทั้งไดเรกทอรีได้ การตรวจสอบเชิงโครงสร้างแบบนี้จับความล้มเหลวที่พบบ่อยที่สุดในโลกจริงได้ นั่นคือนักวิเคราะห์เปลี่ยนชื่อหรือลบ sheet ก่อนส่งการแก้ไขกลับมา ที่ประตูทางเข้า ซึ่งข้อความ error ยังระบุชื่อไฟล์และ sheet ที่หายไปได้ แทนที่จะโผล่ขึ้นมาเป็น nil reference ที่ลึกลงไปอีกสามชั้น

ถ้าคุณกำลังสร้าง pipeline การแปลงที่กว้างขึ้นรอบสิ่งนี้ รูปแบบ workbook audit และ conversion workbench แสดงวิธีสำรวจฟีเจอร์ของไฟล์ก่อนเลือก format เป้าหมาย และ คู่มือประสิทธิภาพของ workbook ขนาดใหญ่ รักษาการ export แบบ batch ให้อยู่ในขอบเขตหน่วยความจำที่สมเหตุสมผล

HotXLS คือไลบรารี spreadsheet แบบ native สำหรับ Delphi และ C++Builder พร้อม source code เต็มรูปแบบ รายการฟีเจอร์ทั้งหมดและรายละเอียดการอนุญาตให้ใช้สิทธิ์อยู่ที่ หน้าผลิตภัณฑ์ HotXLS Delphi Component