Bài viết kỹ thuật

Selection record BIFF8 và cuộn pane trong HotXLS Delphi

HotXLS lưu selection và vị trí cuộn của worksheet theo từng pane qua một API nhận thức pane trên cả TXLSWorksheet lẫn TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow và TryGetWindowScroll. Với file .xls cổ điển, HotXLS ghi các Selection record BIFF8 (0x001D) tối đa 1369 area mỗi record, chuyển tên pane logic thành các byte pane mà định dạng định nghĩa, và giữ mỗi trục cuộn trên record Window2 hay Pane đúng chỗ Excel kỳ vọng

Vấn đề thường lộ diện trong một công cụ đối chiếu hay kiểm toán. Công cụ mở một bản export sổ cái, tìm mọi ô lệch với hệ thống nguồn, và lưu workbook với những ô đó đã được select sẵn dưới một dòng header bị freeze, để người rà soát đáp thẳng vào chỗ khác biệt thay vì phải cuộn đi tìm. Bốn mươi khác biệt thì vẫn ngon. File cuối tháng có 3.000, và một Selection record đơn lẻ giữ 3.000 area không thể tồn tại: body của nó cần 18.009 byte, hơn gấp đôi mức một record BIFF8 mang nổi. Vị trí cuộn cũng có cái bẫy tương tự. Trên một sheet có pane bị freeze, “người dùng đang nhìn đâu” là bốn pane chia sẻ hai vị trí dòng và hai vị trí cột, chứ không phải một tọa độ

Vì sao một selection lớn cần nhiều hơn một Selection record?

Một selection lớn cần vài record vì body của record BIFF8 bị chặn tối đa 8224 byte, và mỗi area được select tốn cố định sáu byte. [MS-XLS] §2.4.248 bày Selection record thành một phần cố định 9 byte (byte pane, rwAct và colAct cho ô active, irefAct cho area active, và cref cho số area) theo sau là cref cấu trúc RefU, mỗi cái giữ hai dòng 16-bit và hai cột 8-bit. Số lượng lớn nhất vừa khít là (8224 − 9) / 6 làm tròn xuống, tức 1369, và cho ra một body 8223 byte, thiếu đúng một byte so với trần. TXLSWorksheet.StoreSelectionGroup lấy hằng số đó làm MaxAreasPerRecord và ghi một nhóm lớn hơn thành các Selection record liên tiếp cho cùng một pane, mỗi lần 1369 area

Chi tiết cắn người là irefAct. Mỗi chunk lặp lại cùng một dòng active, cột active và chỉ mục area active, và irefAct đánh chỉ mục theo chuỗi tổng hợp của mọi chunk, chứ không phải theo các area bên trong record đang mang nó. Một selection vượt giới hạn đúng một area cho thấy điều đó rất cụ thể: 1370 area với area cuối active thành hai record, record đầu với cref 1369 và record sau với cref 1, và cả hai đều mang irefAct 1369. Giá trị đó lớn hơn số area của chính record thứ hai. Một reader đem irefAct đối chiếu cref trong từng record sẽ từ chối một file hợp lệ, còn một reader thay trạng thái của nó ở mỗi record thì đánh rơi 1369 area đầu. Reader của HotXLS nối các record cùng pane liên tiếp vào một nhóm, đòi mọi chunk phải thống nhất về ô active và chỉ mục, và chỉ chạy phép kiểm tra miền tại record EOF của worksheet, khi chuỗi đầy đủ đã rõ. Overload SelectAreas ưu tiên pane vì thế không có trần 1369 area. Nó kiểm chứng mọi tham chiếu A1 và chỉ mục active trước khi giữ khóa ghi của worksheet, và trả False với selection cũ nguyên vẹn nếu có gì đó dị dạng

Vì sao HotXLS ghi một selection worksheet lớn thành nhiều Selection record BIFF8: trần body 8.224 byte vừa đủ 9 byte cố định cộng 1369 area RefU sáu byte, nên 3.000 area thành ba record cùng pane là 1369, 1369 và 262, và irefAct đánh chỉ mục theo chuỗi tổng hợp nên 1370 area với area cuối active cho cả hai record irefAct 1369
Mỗi chunk lặp lại cùng ô active và chỉ mục, reader của HotXLS nối các record cùng pane liên tiếp vào một nhóm, và phép kiểm tra miền chỉ chạy tại record EOF khi chuỗi đầy đủ đã rõ
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // dòng header và cột A đứng yên

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // Freeze đặt lại selection đã lưu, nên hãy select sau khi freeze.
    // 3000 area được lưu thành ba Selection record: 1369 + 1369 + 262
    if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
      raise Exception.Create('Selection rejected');

    Book.SaveAs('reconciliation.xls');
  finally
    Book.Free;
  end;
end;

Một Selection record dùng byte pane nào?

Một Selection record nhận diện pane của nó bằng mã số mà định dạng định nghĩa: 0 cho bottom-right, 1 cho top-right, 2 cho bottom-left, và 3 cho top-left. Enum công khai TXLSPanePosition được khai báo theo thứ tự đọc, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, nên Ord(xlspTopLeft) là 0, tức pane bottom-right trong file. Ép enum thẳng vào byte pane sẽ ghi mọi selection top-left lên pane bottom-right mà không báo lỗi nào. Mọi entry point nhận thức pane của HotXLS chuyển enum qua một câu lệnh case tường minh, nên bên gọi không bao giờ phải đụng tới mã số. Sự tồn tại của pane cũng được kiểm tra: pane top-right chỉ tồn tại khi có split dọc, bottom-left chỉ khi có split ngang, và bottom-right chỉ khi có cả hai. Với một pane mà hình học split hay freeze hiện tại không có, SelectAreas trả False, và GetSelectedAreas trả một mảng rỗng với ActiveAreaIndex bằng -1, mà không tạo ra pane, đối tượng selection hay ô nào trong workbook

HotXLS ánh xạ TXLSPanePosition lên byte pane của Selection BIFF8 thế nào: enum được khai báo theo thứ tự đọc nên Ord(xlspTopLeft) là 0, trong khi file định nghĩa 0 cho bottom-right, 1 cho top-right, 2 cho bottom-left và 3 cho top-left, nên mọi entry point nhận thức pane đều chuyển qua một case statement tường minh
Ép enum thẳng vào byte pane sẽ ghi mọi selection top-left lên pane bottom-right, nên HotXLS còn kiểm tra sự tồn tại của pane chống lại hình học split hay freeze hiện tại trước khi ghi

Vị trí cuộn của từng pane nằm ở đâu?

Vị trí cuộn của từng pane được tách trên hai record, vì bốn pane chỉ chia sẻ hai vị trí dòng và hai vị trí cột. Trong một workbook cổ điển, dòng hiển thị đầu tiên của các pane trên và cột hiển thị đầu tiên của các pane trái là Window2.rwTop và Window2.colLeft, còn dòng của các pane dưới và cột của các pane phải là Pane.rwTop và Pane.colLeft. Vì thế ScrollWindow(xlspTopRight, R, C) ghi Window2.rwTop và Pane.colLeft, và đặt cột cho pane top-right cũng kéo theo pane bottom-right, y như hai bên chia sẻ một thanh cuộn ngang trong Excel. Các method công khai dùng số dòng và cột tính từ 1. Một pane không tồn tại trả False và đặt cả hai kết quả truy vấn về 0, còn một tọa độ ngoài miền bị từ chối trước khi trục nào thay đổi. Chỗ này chẳng phụ thuộc gì vào cách một viewer vẽ lưới. Một control render giữ TopRow và LeftCol của riêng nó, như bài về render workbook trong một VCL grid tùy biến mô tả, và đó là trạng thái runtime, không phải thứ được lưu xuống

Trục cuộn của từng pane trong HotXLS nằm ở đâu: bốn pane chia sẻ hai vị trí dòng và hai vị trí cột, nên dòng trên và cột trái là Window2.rwTop với Window2.colLeft còn dòng dưới và cột phải là Pane.rwTop với Pane.colLeft, và ScrollWindow(xlspTopRight, 1, 6) ghi một trường Window2 cộng một trường Pane nên bottom-right đi theo
XLSX rải cùng dữ liệu đó qua các thuộc tính sheetView và pane topLeftCell, và việc gộp hai lớp thành một chính là cách một vị trí cuộn trên hay trái lặng lẽ biến mất khi nạp

XLSX rải cùng dữ liệu đó qua hai phần tử: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) cho cửa sổ như một tổng thể và phần tử con pane/@topLeftCell (§18.3.1.66) cho phía dưới-phải của một split. Cả hai thuộc tính có thể cùng tồn tại. HotXLS đọc thuộc tính ngoài trước vào các trường cấp cửa sổ, để phần tử con pane chỉ ghi đè các trường cấp pane, và ghi lại cả hai tách rời. Gộp hai lớp thành một chính là cách một vị trí cuộn trên hay trái lặng lẽ biến mất khi nạp. Bản sao worksheet mang cả hai lớp ở cả hai engine. Các entry point cũ giữ nguyên hành vi gốc: các thuộc tính ScrollRow và ScrollColumn cổ điển, và cặp XLSX tính từ 0 là SetPaneScroll và GetPaneScroll. Bản thân hình học freeze và split được cấu hình bằng các thiết lập cấp sheet đã trình bày trong bảo vệ sheet, page setup và printing

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // Bottom-right: trục dòng dưới (Pane.rwTop) và trục cột phải (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Top-right dùng chung trục cột phải, nên lệnh này cũng đưa bottom-right sang cột 6
  Sheet.ScrollWindow(xlspTopRight, 1, 6);

  if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
    Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
    // Bottom-right bắt đầu ở dòng 500, cột 6
end;

Chuyện gì xảy ra khi một Selection record hỏng?

Khi một Selection record hỏng, HotXLS giữ nó như các byte opaque, báo mã diagnostic 1304 (xlsDiagnosticSelectionRecordInvalid), và ghi nguyên body gốc trở lại từng byte khi lưu. Trước khi một record gia nhập nhóm pane của nó, reader kiểm tra nó theo thứ tự. Byte pane phải từ 3 trở xuống. Các record của một pane phải liền kề trong stream. Chín byte cố định phải có mặt. cref phải nằm giữa 1 và 1369, và body phải dài đúng 9 + cref × 6 byte. Mọi chunk trong một nhóm phải thống nhất về ô active và irefAct, irefAct không được bật bit dấu, cột active phải nằm trên lưới, và không area nào được có biên đảo ngược. Vấn đề nằm trong một record vật lý đơn lẻ được báo một lần mỗi record. Những mâu thuẫn chỉ xuất hiện sau khi tổng hợp, chẳng hạn irefAct trỏ vượt tổng số area hay một ô active nằm ngoài area được đánh chỉ mục, được báo một lần mỗi nhóm tại EOF. Một nhóm không hợp lệ vẫn vô hình với API có kiểu: GetSelectedAreas trả một mảng rỗng với chỉ mục -1 cho pane đó, trong khi mọi pane khác vẫn chạy bình thường

var
  I: Integer;
  D: TXLSDiagnostic;
begin
  if Book.Open('supplier-upload.xls') <> 1 then
    Exit;
  for I := 0 to Book.Diagnostics.Count - 1 do
  begin
    D := Book.Diagnostics[I];
    if D.Code = xlsDiagnosticSelectionRecordInvalid then
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

Selection sống sót qua việc chèn dòng và cột ra sao?

Selection sống sót qua các chỉnh sửa cấu trúc vì việc chèn hay xóa nguyên dòng nguyên cột sẽ ánh xạ lại mọi nhóm pane được biểu diễn ở cả engine cổ điển lẫn XLSX thông qua một remapper dùng chung. Các area sống sót giữ nguyên thứ tự và area active giữ nguyên danh tính. Nếu area active bị xóa, area kế tiếp sống sót đầu tiên trở thành active, rồi tới area tiền nhiệm sống sót cuối cùng nếu chẳng còn gì phía sau. Nếu mọi area đều bị xóa, nhóm thu về một ô tại biên xóa, và một ô active không còn nằm trong area được chọn thì dời về góc trên-trái của area đó, nên chỉ mục và tọa độ không bao giờ mâu thuẫn nhau. Các giới hạn là chủ ý. Nhóm cổ điển không hợp lệ bị remapper bỏ qua chứ không bị viết lại thành một selection bịa ra, nên các byte gốc của chúng vẫn round-trip trọn vẹn. Sửa một pane chỉ thay các record của đúng pane đó và để các pane khác giống hệt từng byte. ODS chẳng có trạng thái selection theo pane nào cả, vì ODF không có cấu trúc view worksheet tương đương để mang nó

Nếu ứng dụng của bạn ghi các file .xls mà người dùng mở ra và cần di chuyển trong đó, dù để rà lại các ô được đánh dấu, tiếp tục từ chỗ họ dừng, hay chia sẻ một dashboard bị freeze, API selection và cuộn nhận thức pane là một phần của HotXLS Delphi spreadsheet component, và nó hoạt động như nhau với cả XLS lẫn XLSX