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

ความคิดเห็นและไฮเปอร์ลิงก์ของเซลล์ Excel ใน Delphi ด้วย HotXLS

เปลี่ยนชื่อ sheet จาก "Summary" เป็น "Overview" ใน workbook ที่สร้างขึ้น แล้วทุก hyperlink ภายในที่เคยชี้ไปที่ Summary!A1 จะไม่ชี้ไปที่อะไรเลยอีกต่อไป ไม่มี exception ตอน save ไม่มีตอน open ลิงก์ยังคง render อยู่ ยังดูคลิกได้ และเงียบ ๆ resolve ไปสู่ความว่างเปล่า ความเสียหายแบบเดียวกันนี้ก็ปรากฏขึ้นหลังการแปลงแบบ save-as หรือ round-trip .xls/.xlsx เมื่อ comment ตกไปผิดคอลัมน์หนึ่งช่อง หรือลิงก์แบบ relative หลุดเป้าหมายของมัน ทั้งสองฟีเจอร์นี้บรรจุ review state ที่คนจริง ๆ ใช้ตัดสินใจ ดังนั้นเมื่อมันพัง ความล้มเหลวจะมองไม่เห็นจนกว่าผู้ตรวจจะคลิกแล้วไม่มีอะไรเกิดขึ้น

นั่นคือเหตุผลเชิงปฏิบัติที่ comment และ hyperlink สมควรได้รับการดูแลมากกว่าที่รูปลักษณ์ผิวเผินของมันบ่งบอก HotXLS ให้โค้ด Delphi และ C++Builder เข้าถึงการเขียนทั้งสองสิ่งนี้ได้โดยตรง ทั้งใน XLS และ XLSX โดยไม่ต้องพึ่ง Excel automation เลย ด้านกลับของการควบคุมนี้คือความรับผิดชอบ: ไลบรารีเขียนเป้าหมายตรงตามที่คุณส่งให้มันเป๊ะ ๆ และไม่ตรวจสอบความถูกต้องของมันเลยสักตัว ดังนั้นการรักษา review workflow ให้สมบูรณ์จึงเป็นหน้าที่ของโค้ดคุณ ไม่ใช่หน้าที่ของ Excel

Cell comment ในฐานะบันทึกการตรวจสอบที่เครื่องเขียนขึ้น

ในโมเดลคลาสของ XLSX comment คือ object ระดับ worksheet: มันรู้แถวของมัน คอลัมน์ของมัน ผู้เขียน และเนื้อความ ฟิลด์ author สมควรได้รับที่ยืนของมันเอง เมื่อ workbook ที่โค้ดคุณสร้างขึ้นเดินทางผ่านสายการตรวจสอบ คำถามแรกที่ผู้ตรวจสอบถามคือใครเป็นคนเขียนบันทึกนี้ และบันทึกที่ถูกทิ้งไว้โดยไม่มีผู้เขียนก็ตอบคำถามนั้นด้วยช่องว่าง ประทับตรา identity ของ service ลงบน comment ที่สร้างขึ้นเพื่อให้ที่มาไม่คลุมเครือเลย

แผนภาพการลองซ้ำคอมเมนต์ของ HotXLS ใน Delphi ที่การสอดแนมด้วย FindAt อัปเดตโน้ตเซลล์ที่มีอยู่ ขณะที่การลอง AddComment แบบมั่วซั่วกองสำเนาซ้ำซ้อน
การลองใหม่ที่เรียก AddComment แบบมั่วจะซ้อนโน้ตที่สองทับบนเซลล์เดิม ขณะที่การสำรวจด้วย FindAt ไปแก้โน้ตที่มีอยู่แล้วในจุดนั้น
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Note: TXLSXComment;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('reconciliation.xlsx');
    Sheet := Book.Sheets[0];

    // บันทึกที่มีผู้เขียน สำหรับตัวเลขที่ปรับปรุงแล้ว
    Sheet.AddComment(14, 4, 'Manual adjustment: late FX rate, see ticket FIN-2214',
      'recon-service');

    // อัปเดตบันทึกที่มีอยู่แล้ว แทนที่จะซ้อนบันทึกที่สองเพิ่มเข้าไป
    Note := Sheet.Comments.FindAt(14, 4);
    if Note <> nil then
      Note.Text := Note.Text + ' [verified 2026-06-11]';

    Book.SaveAs('reconciliation-reviewed.xlsx');
  finally
    Book.Free;
  end;
end;

การตรวจสอบด้วย FindAt มีน้ำหนักมากกว่าที่มันดูภายนอก งาน batch ที่ retry หลัง failure ชั่วคราวจะเรียก AddComment เป็นครั้งที่สองบนเซลล์ที่มันเคยใส่คำอธิบายไว้แล้วอย่างสบายใจ และเซลล์นั้นก็จะจบลงด้วยบันทึกสองอันซ้อนกันที่ไม่มีใครต้องการ ตรวจสอบด้วย FindAt ก่อน แล้วอัปเดต object ที่มันคืนกลับมา collection ของ Comments ยังเปิดให้ใช้ DeleteAt และ DeleteInRange ด้วย ตัวแปร range นั้นแหละคือสิ่งที่ควรหยิบใช้เมื่อคุณล้าง workbook ก่อนที่มันจะออกจากองค์กร: การล้างคำอธิบาย QA ภายในทั้งย่านหนึ่งทำได้ด้วยการเรียกครั้งเดียว แทนที่จะเขียน loop วนเซลล์เอง

URL ภายนอกกับการกระโดดภายใน workbook คือ API คนละตัวกัน

OOXML เก็บลิงก์สองชนิดนี้ไว้คนละที่ URL ภายนอกจะกลายเป็นรายการ relationship ใน .rels part ของ sheet โดยเซลล์ชี้ไปที่ relationship นั้นผ่าน id การกระโดดภายในไม่แตะชั้น relationship เลยสักนิด มันเป็นแค่สตริงตำแหน่งธรรมดา เช่น Summary!A1 ที่เก็บไว้ตรง ๆ บนตัวลิงก์เอง HotXLS รักษาความแตกต่างนี้ให้มองเห็นได้ใน API แทนที่จะยัดทุกอย่างลงใน method เดียว ซึ่งหมายความว่าคุณเลือก call ที่ถูกต้องได้จากการรู้ว่าเป้าหมายอยู่ตรงไหน:

แผนภาพเปรียบเทียบว่า HotXLS เก็บ URL ภายนอกเป็น relationship ในส่วน rels และเก็บการกระโดดภายในเป็นสตริง location ธรรมดา ในเวิร์กบุ๊กที่สร้างจาก Delphi
URL ภายนอกเดินทางผ่านชั้น relationship ขณะที่การกระโดดภายในเป็นข้อความธรรมดา แต่ละชนิดจึงพังด้วยวิธีของตัวเองและต้องการกฎตรวจสอบของตัวเอง
Sheet.Cells[2, 1].Value := 'Source record';
Sheet.AddHyperlink(2, 1, 'https://intranet.example.com/records/2214',
  'Open record 2214', 'ERP source entry');

Sheet.Cells[3, 1].Value := 'Totals';
Sheet.AddHyperlinkToCell(3, 1, 'Overview!B12', 'Jump to totals');

บน object TXLSXHyperlink ที่ได้ Url และ Location จะแยกกันแบบ mutually exclusive และ IsInternal จะบอกคุณว่าตัวไหนใน สองตัวนี้ถูกกำหนดค่า ธงนั้นแหละคือสิ่งที่คุณตรวจสอบเมื่อคุณสำรวจรายการลิงก์ใน workbook ที่เปิดอยู่ และต้องแยก "ออกจากไฟล์" กับ "อยู่ในไฟล์" ด้วยกฎคนละชุด: host ภายนอกอาจต้องเจอ allowlist ในขณะที่เป้าหมายภายในแค่ต้องระบุชื่อ sheet ที่มีอยู่จริงก็พอ ลิงก์ภายในไม่พก relationship part ใด ๆ ไว้ข้างหลังเลย ซึ่งทำให้มันเขียนใหม่เป็นจำนวนมากได้ถูกกว่าด้วย

ความเสียหายจากตัวอย่างเปิดเรื่องเกิดขึ้นทั้งหมดที่ฝั่งภายใน และมันสืบเนื่องมาจากข้อเท็จจริงข้อเดียว: สตริงตำแหน่งไม่ใช่ reference ที่ถูก parse HotXLS เขียนข้อความตรงตามที่คุณส่งให้มันเป๊ะ ๆ และไม่มีอะไรชี้ตำแหน่งข้อความนั้นใหม่เมื่อ sheet ถูกเปลี่ยนชื่อภายหลัง มีวิธีป้องกันสองแบบที่ใช้ได้จริง แบบแรกคือวินัยเรื่องลำดับ: เปลี่ยนชื่อทุก sheet ก่อนที่จะสร้างลิงก์แม้แต่อันเดียว จากนั้นปฏิบัติต่อชื่อ sheet เหมือนเป็น identifier ที่ตายตัวแล้ว แบบที่สองแข็งแรงกว่าและรอดจากการเปลี่ยนชื่อที่ทำภายหลังได้ ให้ลิงก์ชี้ไปที่ defined name ระดับ workbook แทนที่จะเป็น address แบบ Sheet!Cell ดิบ ๆ เพราะ Excel จะเขียนคำนิยามของชื่อนั้นใหม่เมื่อ sheet ที่อยู่เบื้องหลังเปลี่ยนไป ลิงก์ก็เลยตามไปโดยอัตโนมัติ วิธีที่สองนี้เข้าคู่กันได้ดีกับเทคนิคใน defined name และสูตรข้าม sheet ใน HotXLS

ฝั่ง XLS: แนวคิดเดียวกัน แต่โครงสร้างพื้นฐานเก่ากว่า

ส่วนหน้า BIFF8 ผูก comment ไว้กับ range แทนที่จะเป็น collection ระดับ worksheet คุณเรียก AddComment บน IXLSRange แล้วได้ TXLSComment กลับมา property Comment ของ range อ่านบันทึกที่มีอยู่แล้ว และ ClearComments ล้างมันทิ้งทั้งหมด จุดที่คมของฝั่งนี้คือเรื่องตำแหน่ง TXLSComment ไม่เปิดเผยแถวและคอลัมน์ของตัวเองแบบ public ดังนั้น loop ตามธรรมชาติที่ว่า "วนทุก comment แล้วรายงานว่ามันอยู่ตรงไหน" จึงวิ่งย้อนกลับสวนทาง API คุณต้องเริ่มจากเซลล์แทน ไม่ว่าจะขับเคลื่อนการตรวจสอบจากรายการที่อยู่ที่คุณใส่คำอธิบายไว้ หรือเก็บ log ตำแหน่งของคุณเองไว้ตอนเขียน เพราะ comment object จะไม่บอกคุณภายหลังว่ามันอยู่ที่ไหน

var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
  Remark: TXLSComment;
begin
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Review';
  Sheet.Cells.Item[5, 2].Value := 4821.50;

  Remark := Sheet.Cells.Item[5, 2].AddComment('Awaiting sign-off from controller');
  Remark.Visible := True;   // เปิดบันทึกให้แสดงตั้งแต่ครั้งแรกที่เห็น

  Sheet.AddHyperlink(7, 2, 'https://intranet.example.com/signoff/4821',
    'Sign-off form', 'Opens the controller queue');
  Book.SaveAs('review.xls');
end;

การตั้งค่า Visible เป็น True คือวิธีแบบดั้งเดิมที่ทำให้บันทึกนั้นถูกมองข้ามไม่ได้: กล่องสีเหลืองยังคงเปิดค้างอยู่บน sheet แทนที่จะรอให้เมาส์ hover TXLSComment ก้าวไปไกลกว่าคู่ของมันใน XLSX อีกขั้นด้วยการเปิด TextRuns ออกมา ดังนั้นบันทึกเดียวจึงพกคำเตือนตัวหนาไว้ข้าง ๆ คำอธิบายแบบธรรมดาได้ ซึ่งเป็นการจัดรูปแบบที่ API ของ comment ฝั่ง XLSX ไม่เปิดให้ใช้ในลักษณะเดียวกัน hyperlink ฝั่งนี้มาผ่าน overload สามระดับที่ค่อย ๆ เพิ่มขึ้น (address อย่างเดียว, จากนั้นพร้อมข้อความแสดงผล, แล้วพร้อม screen tip) และอ่านกลับผ่าน collection HyperLinks ของ worksheet ซึ่งลิงก์แต่ละอันจะเผย Address, SubAddress, DisplayText และ ScreenTip

Sheet ดัชนีการตรวจสอบดีกว่าบันทึกที่กระจัดกระจาย

เกินสิบกว่ารายการคำอธิบายไปแล้ว วิธี hover-to-read ก็เริ่มเอาไม่อยู่แบบเงียบ ๆ บันทึกกองพะเนินอยู่บน sheet ที่ผู้ตรวจสอบไม่เคยเปิด และรายการที่สำคัญที่สุดก็มักเป็นรายการที่พลาดง่ายที่สุดพอดี โครงสร้างที่พิสูจน์ตัวเองมาดีที่สุดคือ sheet ดัชนีที่สร้างขึ้นเอง: หนึ่งแถวต่อหนึ่งตำแหน่งที่มีคำอธิบาย ระบุชื่อ sheet ของมัน address ของเซลล์ ผู้เขียน และข้อความสรุปสั้น ๆ ของบันทึก คอลัมน์สุดท้ายพก hyperlink ภายในที่สร้างด้วย AddHyperlinkToCell ซึ่งกระโดดตรงไปยังเซลล์ที่มีคำอธิบายนั้น ตอนนี้ผู้ตรวจสอบอ่านลงมาตามรายการแทนที่จะต้องไล่ล่าไปทั่ว grid และจำนวนแถวของดัชนีนั้นก็ทำหน้าที่เป็นบัญชี comment สำหรับขั้นตอนตรวจสอบด้านล่างไปพร้อมกันด้วย

ดัชนีนี้สร้างได้ถูกเพราะตัว generator ของคุณรู้อยู่แล้วทุกตำแหน่งที่มันแตะ ต่อท้าย tuple (sheet, row, column, author, summary) เข้าไปในลิสต์ทุกครั้งที่คุณเขียน comment แล้วค่อยปล่อย sheet ดัชนีออกมาเป็นลำดับสุดท้ายเพื่อให้จำนวนแถวของมันนิ่งก่อนที่คุณจะ save การปรับแต่งสองอย่างคุ้มค่าที่จะทำ: จัดเรียงดัชนีตามความรุนแรงหรือตาม sheet แทนที่จะเรียงตามลำดับที่แทรกเข้าไป และใส่ลิงก์ย้อนกลับไว้ในหัวของดัชนีเพื่อให้ผู้ตรวจสอบเด้งกลับขึ้นไปด้านบนได้หลังตรวจแต่ละรายการ เพราะลิงก์ภายในเป็นแค่สตริงตำแหน่งธรรมดาที่ไม่มีอะไรอยู่เบื้องหลังในชั้น relationship เลย ดัชนีแม้จะมีพันแถวก็แทบไม่เพิ่มขนาดไฟล์หรือเวลา save เลย

sheet เดียวกันนี้ให้ผลตอบแทนอีกครั้งในเที่ยวกลับ เมื่อ workbook ที่ตรวจสอบแล้วส่งกลับมา โค้ดของคุณอ่านค่าสถานะที่พิมพ์ไว้ในเซลล์ข้าง ๆ แถวดัชนี แทนที่จะสแกนทุก sheet ใหม่เพื่อหา comment ที่อาจเปลี่ยนไป คอลัมน์ของเซลล์สถานะที่มีโครงสร้างชัดเจน parse ได้สะอาด ในขณะที่บันทึกข้อความอิสระที่กระจัดกระจายทำไม่ได้

ขั้นตอนตรวจสอบก่อนส่งมอบที่จับความเสียหายได้จริง

ไม่มี API ตัวไหนในนี้ตรวจสอบความถูกต้องของเป้าหมายเลย ลิงก์ไปยัง sheet ที่คุณลบไปแล้ว host intranet ที่สะกดผิด file share ที่ถูกปลดระวางไปเมื่อไตรมาสก่อน ทั้งหมดนี้ save ได้โดยไม่มีเสียงบ่นสักคำ ECMA-376 ระบุแค่วิธีเก็บลิงก์ ไม่ได้ระบุว่ามัน resolve ไปสู่อะไรได้จริงหรือไม่ workbook ที่พก review metadata จึงสมควรมีขั้นตอนตรวจสอบสั้น ๆ ของตัวมันเอง รันตรงก่อน SaveAs:

แผนภาพรอบตรวจก่อนส่งมอบของ HotXLS ที่ตรวจเป้าหมายภายใน, allowlist ของ URL, จำนวนคอมเมนต์ และการล้างผู้รับ ก่อน SaveAs ใน Delphi
การตรวจสี่รายการรันก่อน SaveAs เสียอีกนิด และทุกรายการจับความล้มเหลวที่ไลบรารีเองจะไม่มีวัน raise ขึ้นมาเลย
  • รวบรวมตำแหน่งภายในทุกตำแหน่งที่เขียนขึ้นระหว่างการสร้าง และยืนยันว่าชื่อ sheet ที่อยู่หน้าเครื่องหมาย bang ยังคงอยู่ใน sheet collection ของ workbook
  • ตรวจสอบ URL ภายนอกกับ allowlist ของ scheme และ host เส้นทาง file:// เปล่า ๆ และ UNC path เปิดเผยรายละเอียดของสภาพแวดล้อม และพังทันทีที่ไฟล์ออกจากเครือข่ายของคุณ
  • นับจำนวน comment ต่อ sheet แล้วเทียบกับสิ่งที่ generator ของคุณตั้งใจจะเขียน การ retry ที่ทำให้บันทึกเพิ่มเป็นสองเท่าจะโผล่ขึ้นมาตรงนี้ แทนที่จะไปโผล่ในกล่องข้อความของผู้ตรวจสอบ
  • ลบคำอธิบายที่ใช้ภายในเท่านั้นด้วย DeleteInRange เมื่อใดก็ตามที่ผู้รับอยู่นอกองค์กร

ทีมที่สร้าง workbook ของตนจาก data layer สามารถพับขั้นตอนนี้เข้าไปใน pipeline step เดียวกับที่ตรวจสอบข้อมูลอยู่แล้วได้ ทำให้การตรวจสอบ metadata ติดไปด้วยแบบไม่มีค่าใช้จ่ายเพิ่ม กลไกที่ใช้ก็คือกลไกที่อธิบายไว้ใน การส่งออกผลลัพธ์ query จากฐานข้อมูลไปยังรายงาน Excel เพียงแต่หันไปใช้กับลิงก์และ comment แทนที่จะเป็นแถวข้อมูล

รายละเอียดเรื่องการใส่เครื่องหมายคำพูดจุดหนึ่งที่ทำให้คนสะดุด เมื่อพวกเขาสร้างสตริงตำแหน่งด้วยมือเอง sheet ที่มีชื่อมีช่องว่างอยู่ในนั้นต้องถูกใส่เครื่องหมายคำพูดครอบไว้ในสตริงตำแหน่ง เหมือนกับที่ formula bar ใส่เครื่องหมายคำพูดครอบมันเป๊ะ ๆ: 'Quarterly Totals'!A1 ไม่ใช่ Quarterly Totals!A1 HotXLS ใช้กฎเดียวกับที่ formula engine ใช้สำหรับ reference ข้าม sheet ดังนั้นถ้าลิงก์หนึ่งใช้งานได้ในสูตรของ worksheet การใส่เครื่องหมายคำพูดของมันก็จะใช้งานได้ที่นี่เช่นกัน ส่งชื่อที่ไม่ใส่เครื่องหมายคำพูดและมีช่องว่างเข้าไป แล้วคุณจะได้ลิงก์ตายเงียบแบบเดียวกับที่ตัวอย่างเปิดเรื่องเตือนไว้

Comment และ hyperlink คือส่วนของ workbook ที่สร้างขึ้นซึ่งผู้ตรวจสอบลงมือทำตามโดยไม่มองซ้ำสองครั้ง ซึ่งเป็นเหตุผลว่าทำไมเป้าหมายที่ชี้ไปสู่ความว่างเปล่าจึงสร้างความเสียหายจริงก่อนที่ใครจะสังเกตเห็น สร้างขั้นตอนตรวจสอบนี้ครั้งเดียว รันมันกับทุก workbook ก่อนที่มันจะถูกส่งออกไป แล้ว review workflow ก็จะยังคงสมบูรณ์ต่อไปแม้ผ่านการเปลี่ยนชื่อและการแปลงไฟล์ พื้นผิว API ทั้งหมดของทั้งส่วนหน้า XLS และ XLSX มีเอกสารอยู่ที่หน้าผลิตภัณฑ์ HotXLS Delphi Component