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

Grid สเปรดชีตแบบกำหนดเองใน Delphi ด้วย HotXLS

HotXLS มาพร้อม TXLSWorkbookViewer คอนโทรล VCL แบบเนทีฟที่ render workbook แบบ XLS, XLSX, XLSM และ ODS เป็น grid สเปรดชีตแบบโต้ตอบได้ภายในฟอร์ม Delphi หรือ C++Builder โดยไม่ต้องติดตั้ง Excel หรือขับเคลื่อนมันผ่าน OLE automation การสร้างคอนโทรลแบบนั้นให้ดีหมายถึงการแก้ปัญหาเฉพาะเจาะจงสามอย่าง คือการแม็ปการคลิกเมาส์ที่ตกลงภายในเซลล์ที่รวมไว้ให้ไปยังเซลล์ตรรกะที่ถูกต้อง การรักษาตำแหน่ง scroll, แถบ header และการเลือกเซลล์ให้สอดคล้องกันขณะที่ผู้ใช้เลื่อนดูชีตที่ใหญ่กว่าหน้าต่างที่มองเห็นมาก และการตัดสินใจว่าการคลิกบนเครื่องหมายความคิดเห็นหรือเซลล์ไฮเปอร์ลิงก์ควรทำอะไรจริงๆ

บริษัท Delphi ส่วนใหญ่หันไปใช้ตัวดูสเปรดชีตด้วยเหตุผลที่ไม่เกี่ยวกับการแก้ไขเลย เช่น สถานีตรวจสอบที่แสดงตัวอย่าง workbook ที่อัปโหลดก่อนเข้าสู่ pipeline, kiosk หรือตัวดูรายงานที่ Microsoft Office ไม่ได้เป็นส่วนหนึ่งของ image การติดตั้ง หรือเครื่องมือ QA ที่ต้องการแสดงเนื้อหาของ workbook โดยไม่มีความไม่แน่นอนของการขับเคลื่อนโปรเซส Excel จริงผ่าน COM string grid ธรรมดาให้คุณได้ข้อความในเซลล์อย่างรวดเร็ว แต่ไฟล์สเปรดชีตไม่ใช่ grid ธรรมดา เซลล์รวมกันเป็นบล็อกที่มีอยู่แค่ครั้งเดียวในโมเดลข้างใต้ ชีตพกแถบ header คงที่และตำแหน่ง scroll แนวนอนและแนวตั้งอิสระ และเซลล์แต่ละเซลล์พกความคิดเห็นและไฮเปอร์ลิงก์ที่ต้องการโมเดลการโต้ตอบของตัวเอง TXLSWorkbookViewer คือคำตอบของ HotXLS ต่อช่องว่างนั้น และการออกแบบภายในของมันเป็นพิมพ์เขียวที่สมเหตุสมผลสำหรับใครก็ตามที่สร้างคอนโทรลคล้ายกันตั้งแต่ต้น

ตัวดู workbook หลีกเลี่ยงการพึ่งพา Excel ได้อย่างไร

TXLSWorkbookViewer หลีกเลี่ยง Excel โดยสิ้นเชิงด้วยการอ่านผ่าน object model ที่ HotXLS parse เองแทนที่จะเปิดเอกสารผ่าน Excel แล้วบังคับควบคุมมัน property Workbook ผูก TXLSWorkbook ที่มีอยู่แล้วสำหรับไฟล์ XLS แบบดั้งเดิม และ XlsxWorkbook ผูก TXLSXWorkbook สำหรับ XLSX, XLSM และตัวแปรเทมเพลต ตัวใดตัวหนึ่งอาจเปิดอยู่แล้วที่อื่นในแอปพลิเคชัน และตัวดูก็แค่อ่านจากมันเท่านั้น เมื่อคอนโทรลควรเป็นเจ้าของไฟล์เอง LoadFromFile ตรวจสอบนามสกุล ส่ง XLSX, XLSM, XLTX, XLTM และ ODS ผ่านเอนจิ้นสมัยใหม่ และอย่างอื่นทั้งหมดผ่านเอนจิ้นแบบดั้งเดิม แล้ว free workbook ใดก็ตามที่มันสร้างขึ้นเมื่อคอนโทรลถูกเคลียร์หรือทำลาย

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

การหาเซลล์ที่ถูกต้องภายในช่วงที่รวมไว้

การแก้การคลิกให้เป็นเซลล์ที่ถูกต้องใน TXLSWorkbookViewer เป็นการค้นหาสองขั้นตอน และการแบ่งนี้สำคัญเพราะ geometry ของพิกเซลกับความหมายของสเปรดชีตเป็นปัญหาที่ต่างกันจริงๆ ขั้นตอนแรกเป็น geometry ล้วนๆ method ส่วนตัวชื่อ CellAtPoint เดินผ่านความกว้างคอลัมน์และความสูงแถวจากตำแหน่ง scroll ปัจจุบันจนกว่าจะพบแถบที่มีพิกัด X และ Y ที่คลิกอยู่ โดยไม่รู้เรื่องเซลล์ที่รวมไว้เลย ขั้นตอนที่สองเป็นความหมาย ทุกเส้นทางที่เปลี่ยนการเลือก การคลิกเมาส์, ปุ่มลูกศร, Tab หรือการเรียก GoToCell ตรงๆ ล้วนไหลผ่านรูทีนภายในตัวเดียวคือ ChangeSelection ซึ่ง normalize แถวและคอลัมน์ดิบเทียบกับการรวมใดๆ และ snap มันเข้ากับเซลล์ anchor ของการรวมนั้นก่อนที่การเลือกจะเปลี่ยนจริง

anchor คือเซลล์บนซ้ายของช่วงที่รวมไว้ และเป็นเซลล์เดียวในบล็อกนั้นที่ถือค่า, format, ความคิดเห็น หรือไฮเปอร์ลิงก์จริงๆ ในโมเดล workbook ข้างใต้ ทุกเซลล์อื่นที่การรวมครอบคลุมด้วยสายตาว่างเปล่าในข้อมูลจริง สำหรับ workbook XLS แบบดั้งเดิม anchor มาจาก Cell.MergeArea ซึ่งเป็น IXLSRange ที่ Row และ Column ของมันชี้ไปยังเซลล์เจ้าของ สำหรับ workbook XLSX และ ODS MergedCells.FindAt คืน TXLSXMergedRange ที่เปิด anchor เดียวกันเป็น Row1 และ Col1 การวาดแก้ปัญหาที่เทียบเท่ากันอย่างอิสระ ขยายสี่เหลี่ยมของเซลล์ที่รวมไว้ให้ครอบคลุมแถวและคอลัมน์เต็มช่วงของมัน และข้ามเซลล์ภายในช่วงนั้น ดังนั้นเส้นขอบการเลือกจึงห่อทั้งบล็อกที่รวมไว้ ไม่ใช่แค่มุม anchor ของมันเท่านั้น และการเขียน layout ที่รวมไว้แทนที่จะแค่อ่านมันกลับเป็นปัญหาที่เกี่ยวข้องกันแต่แยกต่างหาก ครอบคลุมในบทความคู่กันเรื่องlayout ของเซลล์ที่รวมไว้สำหรับเทมเพลตรายงาน

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

อะไรที่รักษาการ scroll, header และการเลือกให้สอดคล้องกัน

TXLSWorkbookViewer รักษา state สามส่วนแยกกันให้สอดคล้องกัน คือตำแหน่ง scroll เชิงตรรกะที่ถือไว้ใน TopRow และ LeftCol, scrollbar แบบเนทีฟของ Windows ที่คอนโทรลขอผ่าน WS_HSCROLL และ WS_VSCROLL ใน CreateParams และการเลือกปัจจุบันใน SelectedRow และ SelectedCol การลาก scrollbar หรือหมุนล้อเมาส์ยิง WM_HSCROLL, WM_VSCROLL หรือ WM_MOUSEWHEEL ซึ่งอัปเดต TopRow หรือ LeftCol และ repaint การเลือกไม่ขยับ ซึ่งตรงกับวิธีที่ Excel เองแยกการเลื่อนดูออกจากการเลือก หลังการอัปเดตใดๆ เหล่านั้น UpdateScrollBars จะผลักตำแหน่งใหม่กลับเข้าไปใน scrollbar แบบเนทีฟผ่าน SetScrollInfo ดังนั้น thumb จึงไม่มีทางเลื่อนไหลออกจากสิ่งที่ grid กำลังแสดงจริงๆ

การนำทางด้วยคีย์บอร์ดรันการซิงค์เดียวกันในทิศทางตรงข้าม การย้ายการเลือกเลยขอบของ grid ที่มองเห็นได้เรียก EnsureSelectionVisible ซึ่งขยับ TopRow หรือ LeftCol ด้วยการสะสมความกว้างคอลัมน์และความสูงแถวจริงแทนที่จะแค่บวกเพิ่มทีละหนึ่ง เพราะแถวและคอลัมน์สามารถพกขนาดกำหนดเองได้ แล้วเรียก UpdateScrollBars เพื่อให้ thumb สะท้อนตำแหน่งที่คีย์บอร์ดเพิ่งพา view ไป แถบ header หมายเลขแถวและตัวอักษรคอลัมน์ ขนาดผ่าน RowHeaderWidth และ ColumnHeaderHeight คือส่วนของคอนโทรลนี้ที่คงที่อยู่บนหน้าจอในขณะที่ TopRow และ LeftCol เลื่อนข้อมูลข้างใต้ และนั่นคือขอบเขตของการ freeze ที่คอนโทรลนี้ทำเองได้ มันไม่ใช่ฟีเจอร์ Freeze Panes ของ Excel และไม่มีวิธีในตัวที่จะปักหมุดแถวหรือคอลัมน์ข้อมูลใดๆ ในขณะที่ชีตที่เหลือเลื่อนผ่านมัน ขอบเขตหนึ่งที่ควรทดสอบก่อนส่งตัวดูออกไปกับไฟล์ที่คุณควบคุมไม่เต็มที่ คือ TopRow และ LeftCol ไม่ได้ถูก clamp เทียบกับช่วงที่ใช้จริงของเวิร์กชีต ดังนั้น thumb ที่ลากไปถึงขีดจำกัดเชิงโครงสร้างอาจลงเอยที่แถว 1,048,576 หรือคอลัมน์ 16,384 และแสดง grid ว่างเปล่าแทนที่จะเป็นแถวหรือคอลัมน์สุดท้ายที่มีข้อมูลจริง workbook ที่ใหญ่พอที่จะทำให้สิ่งนี้สังเกตเห็นได้ มักใหญ่พอที่จะต้องการความใส่ใจฝั่งการโหลดที่ครอบคลุมในบทความประสิทธิภาพ workbook ขนาดใหญ่ด้วย

การต่อสายความคิดเห็นและไฮเปอร์ลิงก์เข้ากับ event ของเมาส์และการเลือก

TXLSWorkbookViewer ปฏิบัติต่อความคิดเห็นและไฮเปอร์ลิงก์เป็น attribute ของเซลล์ที่ถูกเลือกอยู่ในขณะนั้น แทนที่จะเป็นเป้าหมายการ hover ดังนั้น SelectedCellCommentText, SelectedCellCommentAuthor และ SelectedCellHyperlink จึงอัปเดตทุกครั้งที่ OnSelectionChange ยิง ไม่ว่าการเลือกจะขยับด้วยการคลิกเมาส์ ปุ่มลูกศร หรือการเรียก GoToCell เซลล์ที่มีความคิดเห็นจะได้สามเหลี่ยมสีแดงเล็กๆ วาดไว้ที่มุมบนขวาเป็นสัญญาณทางภาพ คล้ายกับ flag ความคิดเห็นของ Excel เอง แต่เครื่องหมายนั้นเป็นภาพล้วนๆ ไม่มี tooltip ที่กระตุ้นด้วยการ hover สร้างไว้ในคอนโทรลเลย ดังนั้นแอปพลิเคชันที่ต้องการ popup เมื่อเมาส์วางทับแทนที่จะเป็นตอนเลือก ต้องสร้างชั้นนั้นเอง การเปิดใช้ไฮเปอร์ลิงก์ทำงานแบบเลือก-ก่อนเดียวกัน การดับเบิลคลิกเซลล์เรียก ActivateSelectedCell ซึ่งอ่าน SelectedCellHyperlink และถ้ามันไม่ว่างเปล่า จะยก OnHyperlinkClick พร้อม address เป้าหมายและพารามิเตอร์ var Handled: Boolean ให้ handler ตั้งค่า

สิ่งที่ OnHyperlinkClick ไม่ทำก็สำคัญพอๆ กัน TXLSWorkbookViewer ไม่เคยเรียก ShellExecute หรือเปิดเบราว์เซอร์เองเลย ไม่ว่า handler จะตั้ง Handled เป็น true หรือปล่อยเป็น false การนำทาง และการตัดสินใจใดๆ ว่าอะไรนับเป็นเป้าหมายที่ปลอดภัย เป็นความรับผิดชอบของแอปพลิเคชัน host ทั้งหมด ซึ่งเป็นค่าเริ่มต้นที่ถูกต้องสำหรับคอมโพเนนต์ที่ไม่รู้เลยว่ามันถูกฝังอยู่ในเครื่องมือภายในที่เชื่อถือได้ หรือตัวดูสำหรับไฟล์ที่ลูกค้าเพิ่งอัปโหลด

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

ขอบเขตการเลือกและขีดจำกัดการนำทางด้วยคีย์บอร์ด

การเลือกใน TXLSWorkbookViewer เป็นเซลล์ตรรกะเดียวเสมอ ติดตามเป็น SelectedRow และ SelectedCol ไม่มีการเลือกช่วงหลายเซลล์แบบสี่เหลี่ยมในคอนโทรลพื้นฐาน ดังนั้นฟีเจอร์ใดๆ ที่ต้องทำงานกับบล็อกของเซลล์ต้องถูกสร้างขึ้นเหนือมัน แทนที่จะอ่านจาก object การเลือก การครอบคลุมด้านคีย์บอร์ดตั้งใจให้พื้นฐาน ปุ่มลูกศรขยับทีละเซลล์ Home กลับไปจุดเริ่มต้นของแถว หรือด้วย Ctrl ไปยังเซลล์ A1, Page Up และ Page Down กระโดดสิบแถว และ Tab กับ Shift+Tab ก้าวข้ามคอลัมน์ ไม่มีการกระโดด Ctrl+Arrow ไปยังขอบของพื้นที่ข้อมูล และไม่มีการเลือกช่วงแบบขยายด้วย Shift ดังนั้นผู้ใช้ที่มาจาก Excel โดยตรงจะสังเกตเห็นช่องว่างนี้บนชีตที่หนาแน่น

ขีดจำกัดคอลัมน์ถูกบังคับใช้ที่จุดคอขวด ChangeSelection เดียวกับที่จัดการการ normalize การรวม และแตกต่างกันตามเอนจิ้นโดยตั้งใจ ตัวดูที่ผูกกับ TXLSWorkbook แบบดั้งเดิม clamp ที่คอลัมน์ 256 ซึ่งเป็นเพดานเชิงโครงสร้างของฟอร์แมต BIFF8 ในขณะที่ตัวที่ผูกกับ TXLSXWorkbook เคารพขีดจำกัด 16,384 คอลัมน์สมัยใหม่ที่ XLSX สืบทอดมาจาก Excel 2007 เป็นต้นมา แถวถูกจำกัดที่ 1,048,576 ทั้งสองแบบ ดังนั้นความแตกต่างในทางปฏิบัติระหว่างการเปิดไฟล์ XLS รุ่นเก่ากับไฟล์ XLSX ในตัวดูเดียวกัน อยู่ที่ว่า grid ยอมให้คุณไปทางขวาได้ไกลแค่ไหนเท่านั้น

ไม่มีอะไรในนี้ที่แปลกประหลาดเลยเมื่อแยกย่อยเป็นการค้นหาพิกเซล, การ normalize anchor และ message handler จำนวนหนึ่ง แต่การทำให้ทั้งสามอย่างตรงกันภายใต้ไฟล์จริง พร้อมการรวม, ความคิดเห็น และไฮเปอร์ลิงก์จริง คือส่วนใหญ่ของงานในคอมโพเนนต์แบบนี้ TXLSWorkbookViewer มาพร้อมกับHotXLS Excel Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder ควบคู่ไปกับ object model แบบดั้งเดิมและ XLSX ที่มัน render มา