สเปรดชีตหนึ่งไฟล์พกตัวตนไว้สองชั้น มีตารางเซลล์อยู่ชั้นหนึ่ง และมี document metadata ที่เดินทางไปพร้อมกันอีกชั้นหนึ่ง: title, author, company, keyword, timestamp ต่าง ๆ Excel ไม่เคยแสดงชั้นที่สองนั้นในตารางเลย แต่มันคือชั้นที่ Windows Search นำไป index คือชั้นที่ SharePoint อ่านเพื่อตั้งชื่อเรื่องเอกสาร และคือชั้นที่ระบบจัดการบันทึกใช้แฟ้มตาม เมื่อ workbook ที่สร้างขึ้นสืบทอด Author และ Title มาจาก template ที่มันถูกสร้างขึ้นมาจาก ทุกระบบปลายทางก็จะบันทึกว่าคนออกแบบ template คือผู้เขียนใบแจ้งยอดลูกค้าสี่พันฉบับ metadata ไม่ถูกต้องเลยสักที่เดียว แต่ถูกอ้างอิงถึงทุกที่
HotXLS เปิดชั้นนี้ออกมาเป็น property ระดับ workbook ธรรมดาบนเอนจิ้นทั้งสองตัวของมัน: facade ของ BIFF สำหรับ .xls และ facade ของ OOXML สำหรับ .xlsx คุณอ่านฟิลด์หนึ่งหลังจากเปิดไฟล์ และเขียนฟิลด์หนึ่งก่อน save ไฟล์ ไลบรารีเป็นคนตัดสินใจว่าค่านั้นจะไปตกอยู่ที่ container ทางกายภาพไหน สิ่งที่ควรเข้าใจก่อนเขียนตัวสร้างรายงานคือ ฟอร์แมตแต่ละแบบรองรับฟิลด์ไหนจริง ๆ ฟิลด์เหล่านั้นอยู่ที่ไหนทางกายภาพ และกฎประตูเดียวที่ควบคุมว่า .xlsx จะบันทึก metadata ใด ๆ เลยหรือไม่
สองฟอร์แมต สองโมเดลการจัดเก็บ
เหตุผลที่ไลบรารีสเปรดชีตต้องมีการ implement metadata สองแบบ และเหตุผลที่เครื่องมือที่ทำไม่ครบมักประทับฟอร์แมตหนึ่งได้ถูกต้องแต่ลืมอีกฟอร์แมตหนึ่ง ก็คือ .xls และ .xlsx เก็บ property ของมันไว้คนละที่กันโดยสิ้นเชิง workbook แบบ BIFF เขียนมันลงใน stream ของ OLE compound-file โดยหลัก ๆ คือ property set ชื่อ SummaryInformation ที่มีมาก่อน Excel เองด้วยซ้ำ พร้อมกับ record WRITEACCESS ที่อยู่ใน stream ซึ่งระบุชื่อคนที่ save ไฟล์ล่าสุด workbook แบบ OOXML เก็บมันไว้เป็น XML part ข้างในแพ็กเกจ zip แบ่งตามวัตถุประสงค์: docProps/core.xml เก็บฟิลด์ Dublin Core (title, creator, subject, keywords, วันที่) และ docProps/app.xml เก็บฟิลด์ระดับแอปพลิเคชันอย่าง company และแอปที่สร้างไฟล์ ตามข้อกำหนด ECMA-376 Part 1
HotXLS ทำให้โมเดลการจัดเก็บทั้งสองแบบแบนราบลงเป็น property โดยตรงของ workbook object คุณไม่เคยต้องเปิด stream ของ property-set หรือแก้ XML part ด้วยมือเลย คุณแค่กำหนดสตริงและวันที่ให้กับ workbook แล้ว container ที่ถูกต้องก็จะปรากฏขึ้นเองไม่ว่าคุณจะ save เป็นฟอร์แมตไหน
ประทับ workbook ที่สร้างขึ้นด้วยข้อมูลจากบันทึกทางธุรกิจ
ในฝั่ง XLSX นั้น TXLSXWorkbook เปิดให้ใช้ Title, Subject, Author, Keywords, Description, Category, LastModifiedBy, Company, Application และ AppVersion เป็นสตริง บวกกับ Created และ Modified เป็นค่า TDateTime ที่ค่าศูนย์หมายถึงยังไม่ได้ตั้งค่า กฎที่ปิดช่องโหว่การสืบทอดนี้มีแค่ประโยคเดียว: กำหนดค่าทุกฟิลด์ในทุกการรัน โดยเอาค่ามาจากบันทึกทางธุรกิจ แทนที่จะเชื่อว่า template จะพกอะไรมาให้
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('statement-template.xlsx') <> 1 then
raise Exception.Create('Template not available');
// เขียนทับทุกฟิลด์: อะไรก็ตามที่ไม่ถูกแตะ
// จะถูกสืบทอดมาจากคนที่ออกแบบ template
Book.Title := 'Account Statement 2026-06 / ACME Corp';
Book.Subject := 'Monthly account statement';
Book.Author := 'Billing Service 4.2';
Book.LastModifiedBy := 'Billing Service 4.2';
Book.Company := 'Northwind Financial';
Book.Category := 'Customer Delivery';
Book.Keywords := 'statement;billing;2026-06;acct-10024';
Book.Description := 'Generated document - manual edits are not retained';
Book.Created := Now;
Book.Modified := Now;
Book.SaveAs('statement-10024.xlsx');
finally
Book.Free;
end;
end;
ฟิลด์ Keywords คุ้มค่ากับการคิดให้รอบคอบมากกว่าที่มันมักได้รับ ระบบค้นหา index มันตรงตัวเป๊ะ ๆ ทั้ง Windows Search, SharePoint และผลิตภัณฑ์ DMS ส่วนใหญ่ ดังนั้นธรรมเนียมแยกด้วยเซมิโคลอนที่พกเลขบัญชีและช่วงเวลาไว้ ก็เปลี่ยน workbook ที่ส่งมอบทุกไฟล์ให้กลายเป็นบันทึกที่ค้นหาเจอได้โดยไม่ต้อง round trip ไปที่ฐานข้อมูลเลย การเข้าถึงแบบเดียวกันนี้แหละคือข้อดักจับ property เดินทางไปพร้อมกับสำเนาไฟล์ทุกชุด ไกลเกินการควบคุมสิทธิ์เข้าถึงของระบบที่เขียนมันไปมากทีเดียว ดังนั้นข้อมูลส่วนบุคคลจึงไม่ควรอยู่ในนั้นเลย
คู่ timestamp พกความหมายที่ควรกำหนดไว้เป็นนโยบายอย่างชัดเจน มากกว่าปล่อยให้เป็นแค่ความเคยชิน Created ควรบันทึกช่วงเวลาที่ pipeline ของคุณสร้างเอกสารขึ้นมา แล้วก็หยุดนิ่งตั้งแต่นั้น Modified คือฟิลด์ที่ Excel อัปเดตทุกครั้งที่ผู้รับ save ไฟล์ ดังนั้นความต่างระหว่างสองค่านี้หลังจากส่งมอบไปแล้วก็คือหลักฐานยืนยันว่ามีคนแก้ไข workbook ต่อที่ปลายทาง ซึ่งช่วยยุติข้อโต้แย้งหลายครั้งได้ว่าตัวเลขในสเปรดชีตที่ถูกส่งต่อมาจริง ๆ แล้วเป็นของใครกันแน่ มีกับดักหนึ่งซ่อนอยู่ในสถานะที่ยังไม่ได้ตั้งค่า: มันคือค่าศูนย์ตัวอักษรจริง ๆ ไม่ใช่ exception และไม่ใช่ null ดังนั้นโค้ด audit ต้องตรวจสอบค่าศูนย์อย่างชัดแจ้ง ถ้า format TDateTime ที่ยังไม่ได้ตั้งค่าโดยไม่มีการ์ดนั้น log ของคุณก็จะเต็มไปด้วยวันที่เดือนธันวาคม 1899 ที่ผิดอย่างมั่นใจ
DocPropsTouched: workbook ที่ส่งออกไปโดยไม่มี docProps
flag แบบอ่านอย่างเดียวชื่อ DocPropsTouched คุมประตูของตัวเขียน property ของ XLSX workbook ที่ไม่เคยมีการกำหนด property ใดเลยสักตัวจะไม่สร้าง docProps part ใด ๆ ขึ้นมาเลย HotXLS ปฏิเสธที่จะเขียนโครง metadata ที่ว่างเปล่า พฤติกรรมนี้เรียบร้อยดี แต่มันมีผลตามมาสองอย่างที่ควรออกแบบรองรับไว้
โค้ดรับข้อมูลเข้าฝั่งผู้บริโภคต้องไม่สมมติว่า core.xml มีอยู่ในทุกแพ็กเกจ เครื่องมือที่บังคับต้องมีมันจะปฏิเสธไฟล์ minimal ที่ถูกต้องสมบูรณ์แบบไปเสียเปล่า ๆ และถ้าสถานะ compliance ของคุณต้องการให้เอกสารขาออกทุกฉบับพกตัวตนของตัวสร้างอย่างน้อยหนึ่งอย่าง ความต้องการนั้นก็ต้องกลายเป็นโค้ด ไม่ใช่คุณสมบัติของฟอร์แมต: กำหนด Application และ Author แบบไม่มีเงื่อนไขใน save path เพราะ workbook ที่ไม่ถูกแตะเลยถือว่าถูกกฎหมายอย่างสมบูรณ์ตามข้อกำหนด ในขณะที่แอบละเมิดนโยบายของคุณไปแบบเงียบ ๆ
surface ของ XLS แบบดั้งเดิม และกับดักของ Comments
facade ของ BIFF พกชุดฟิลด์ที่เก่ากว่าและเล็กกว่า: Title, Subject, Author, Keywords, Comments, Company และ Manager บวกกับ LastSavedBy ซึ่งเป็น alias ของ UserName ที่เขียน record WRITEACCESS ที่ Excel แสดงเมื่อผู้ใช้อีกคนล็อกไฟล์ไว้อยู่
var
Legacy: IXLSWorkbook; // interface นับ reference: ไม่ต้อง Free ด้วยมือ
begin
Legacy := TXLSWorkbook.Create;
if Legacy.Open('archive-1999.xls') <= 0 then
raise Exception.Create('Cannot open archive file');
Legacy.Title := 'FY1999 ledger (migrated copy)';
Legacy.Author := 'Archive Migration Batch';
Legacy.Company := 'Northwind Financial';
Legacy.Comments := 'Migrated 2026-06-11; source retained in cold storage';
Legacy.LastSavedBy := 'migration-svc'; // record WRITEACCESS ของ BIFF
Legacy.SaveAs('archive-1999-stamped.xls');
end;
มีการชนกันของชื่อหนึ่งจุดที่มักก่อให้เกิดความสับสนซ้ำแล้วซ้ำเล่า property Comments ระดับเอกสารตรงนี้คือหมายเหตุแบบข้อความอิสระที่แสดงใน property dialog ของไฟล์ มันไม่เกี่ยวอะไรกับ cell comment เลย ซึ่งเป็นอ็อบเจ็กต์ระดับ drawing layer ที่ผูกกับช่วงเซลล์ผ่าน API ที่แยกกันไปคนละชุดโดยสิ้นเชิง การรีวิวโค้ดที่ยอมรับคำว่า "เราเขียน Comments ไปแล้ว" โดยไม่ตรวจสอบว่าหมายถึงตัวไหน ก็คือการยอมรับข้อกล่าวอ้างเกี่ยวกับฟีเจอร์ที่ผิดไปแล้ว และเรื่องแบบนี้เกิดขึ้นบ่อยกว่าที่ชื่อที่ใช้ร่วมกันจะบอกใบ้ ทั้งสองอย่างมีตัวอักษรที่เหมือนกันอยู่สี่ตัว แต่ไม่มีพื้นที่จัดเก็บร่วมกันแม้แต่ไบต์เดียว
อ่าน metadata ตอนรับเข้า และช่องว่างของการตรวจสอบ
การอ่านก็สมมาตรกัน หลังจาก Open property ชุดเดียวกันจะถูกเติมค่ากลับมาจากไฟล์ ซึ่งทำให้การ audit metadata ของ workbook ขาเข้ากลายเป็น loop สั้น ๆ
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) = 1 then
begin
Writeln(Format('%s | title="%s" author="%s" created=%s',
[ExtractFileName(FileName), Book.Title, Book.Author,
FormatDateTime('yyyy-mm-dd', Book.Created)]));
if Book.Created = 0 then
Writeln(' no creation date recorded');
end;
finally
Book.Free;
end;
end;
วางแผนรอบข้อจำกัดหนึ่งอย่างไปพร้อมกัน ไม่มีการตรวจแบบ properties-only เลย GetSheetNames แสดงรายชื่อ sheet ได้โดยไม่ต้องโหลด workbook แต่การอ่าน Title หรือ Author หมายถึงต้อง Open แบบเต็ม ดังนั้นการคัดกรอง metadata ข้าม archive ขนาดใหญ่จึงต้องจ่ายต้นทุนการ parse เต็มรูปแบบทุกไฟล์ ในฝั่ง BIFF คุณลดต้นทุนนั้นสำหรับการ audit แบบอ่านอย่างเดียวได้ด้วยการตั้ง _DisableGraphics เป็น true ก่อนเปิดไฟล์ ซึ่งข้าม drawing layer ไปเลยทันที มันเหมาะกับ loop ที่แค่อ่าน property และสถิติเซลล์เท่านั้น และมันผิดทันทีถ้าอินสแตนซ์เดียวกันนั้นอาจต้อง save ด้วย เพราะเนื้อหา drawing ที่ถูกข้ามไปจะหายไปด้วย เมื่อโครงสร้าง sheet เพียงอย่างเดียวก็กรองชุดข้อมูลล่วงหน้าได้ เช่นการ export แบบ single-sheet ที่ควรข้ามอย่างชัดเจน เทคนิคราคาถูกใน บทความของเราเรื่อง sheet listing และการตรวจสอบแบบเบา ก็ช่วยลดจำนวนไฟล์ที่ต้องเข้าสู่รอบที่แพงได้ และในงาน stamping จำนวนมาก ที่ผลลัพธ์หลายพันไฟล์ถูกเขียนขึ้นแทนที่จะถูกตรวจสอบ รูปแบบ throughput ฝั่งเขียนใน บทความของเราเรื่อง streaming write สำหรับงาน batch ก็นำมาใช้ได้เลยโดยไม่ต้องเปลี่ยนอะไร เพราะการกำหนด property ไม่เพิ่มเวลา save ที่วัดได้เลยแม้แต่น้อย
ข้ามฟอร์แมต และควบคุมการรั่วไหล
property round-trip ได้อย่างสะอาดภายใน facade เดียวกัน: เปิด .xlsx แก้ไข save แล้วชุด property ก็กลับมาครบถ้วน การข้ามฟอร์แมตคือจุดที่สมมติฐานเรื่องความเท่าเทียมกันพังลง เพราะชุดฟิลด์ของ BIFF และ OOXML ไม่ตรงกันแบบหนึ่งต่อหนึ่งเลย BIFF มี Manager แต่ไม่มี timestamp ใด ๆ ส่วน OOXML มี Category, Description และคู่ Created/Modified ตัวแปลงที่คัดลอกแบบไม่มองอะไรเลยจะทำให้สิ่งที่ฟอร์แมตปลายทางเก็บไม่ได้หายไป ดังนั้นให้ map ฟิลด์อย่างชัดแจ้ง แล้วใส่การ map นั้นไว้ใน checklist การแปลงของคุณ ควบคู่ไปกับทุกอย่างอื่นที่ไม่รอดจากการเดินทางนี้
การรั่วไหลที่การสืบทอด template เปิดออกมานั้นวิ่งไปในทางตรงข้าม: ข้อมูลที่คุณไม่เคยตั้งใจจะส่งออกไปเลย ชื่อผู้เขียน ป้ายชื่อโปรเจกต์ภายในที่จอดอยู่ใน keyword ชื่อ draft ที่ไม่มีใครล้างทิ้ง วินัยการเขียนทับทุกอย่างจากตัวสร้างรายงานด้านบนคือการป้องกันทั้งหมดที่มี และควรค่าแก่การตรวจสอบแบบเดียวกับที่คนนอกจะทำ นั่นคือเปิด Properties dialog ที่ลูกค้าคนไหนก็เข้าถึงได้ หรือแตก zip ไฟล์ .xlsx แล้วอ่าน docProps/core.xml ตรงจากแพ็กเกจเลย สิ่งที่คุณเห็นตรงนั้นคือสิ่งที่ indexer ปลายทางทุกตัวเห็นเป๊ะ ๆ
การมองเห็นได้จากปลายทางแบบนี้ก็เป็นเหตุผลที่ฟิลด์บางตัวควรได้รับการดูแลมากกว่าตัวอื่น Title, Author, Keywords (ซึ่งปรากฏเป็น Tags) และ Comments หรือ Description แบกน้ำหนักของการ index ส่วนใหญ่ใน SharePoint และ Windows Search ไว้ Title ที่แตกต่างกันจริง ๆ ในแต่ละเอกสาร พกช่วงเวลาและเลขบัญชีไว้ด้วย ช่วยเรื่องการค้นหาได้มากกว่าสคีมาการตั้งชื่อโฟลเดอร์ใด ๆ ที่ซ้อนทับไว้ข้างบนมันเสียอีก และมันมีต้นทุนแค่การกำหนดค่าหนึ่งครั้งต่อการ save เท่านั้น
document property คือการเก็บรายละเอียดแบบมืออาชีพที่ถูกที่สุดที่ workbook ที่สร้างขึ้นสามารถพกไปได้ และก็เป็นข้อบกพร่องที่ถูกส่งออกไปบ่อยที่สุดเมื่อไม่มีใครเป็นเจ้าของมัน property surface ทั้งสองแบบที่อธิบายไว้ในบทความนี้เป็นส่วนหนึ่งของ HotXLS Delphi Component ซึ่งเขียนมันแบบ native ทั้งสำหรับ XLS และ XLSX โดยไม่ต้องใช้ Excel automation