Техническа статия

BIFF8 Selection записи и pane scroll в HotXLS за Delphi

HotXLS съхранява селекциите на листа и скрол позициите по pane чрез един pane-aware API както на TXLSWorksheet, така и на TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow и TryGetWindowScroll. За класически .xls файлове HotXLS записва BIFF8 Selection записи (0x001D) с най-много 1369 области всеки, превръща логическите pane имена в pane байтовете, които форматът дефинира, и пази всяка скрол ос на Window2 или Pane записа, където Excel го очаква

Проблемът обикновено изниква в инструмент за сверяване или одит. Инструментът отваря ledger export, намира всяка клетка, която не съвпада с source системата, и записва работната книга с тези клетки вече селектирани под замразен header ред, така че проверяващият каца директно върху разликите вместо да ги търси със скрол. При четиридесет разлики работи гладко. Файлът за месечното приключване има 3000, а един-единствен Selection record с 3000 области не може да съществува: тялото му би искало 18009 байта, над два пъти повече от това, което един BIFF8 запис може да понесе. Скрол позицията има подобен капан. На лист със замразени panes "къде е гледал потребителят" са четири pane-а, споделящи две редови и две колонови позиции, не една координата

Защо голямата селекция се нуждае от повече от един Selection запис?

Голямата селекция се нуждае от няколко записа, защото тялото на BIFF8 запис има таван от 8224 байта, а всяка селектирана област струва фиксирано шест байта. [MS-XLS] §2.4.248 разлага Selection записа на 9-байтова фиксирана част (pane байта, rwAct и colAct за активната клетка, irefAct за активната област и cref за броя области), следвана от cref на брой RefU структури, всяка с два 16-битови реда и две 8-битови колони. Най-големият брой, който се побира, е (8224 − 9) / 6 закръглено надолу, тоест 1369, което дава тяло от 8223 байта, един байт под лимита. TXLSWorksheet.StoreSelectionGroup ползва тази константа като MaxAreasPerRecord и записва по-голяма група като поредни Selection записи за същия pane, по 1369 области наведнъж

Деталят, който хапе, е irefAct. Всяко парче повтаря същите активен ред, активна колона и индекс на активна област, а irefAct индексира агрегираната последователност на всички парчета, не областите вътре в записа, който го носи. Селекция с една област над лимита прави това конкретно: 1370 области с последната активна стават два записа, първият с cref 1369 и вторият с cref 1, и двата носят irefAct 1369. Тази стойност е по-голяма от собствения брой области на втория запис. Reader, който проверява irefAct срещу cref във всеки запис, отхвърля валиден файл, а reader, който заменя състоянието си при всеки запис, губи първите 1369 области. HotXLS reader-ът долепя поредните записи от същия pane в една група, изисква всяко парче да се съгласява по активната клетка и индекс и пуска проверката за диапазон чак на EOF записа на листа, когато цялата последователност е известна. Pane-first SelectAreas overload-ът затова няма таван от 1369 области. Той валидира всяка A1 референция и активния индекс, преди да вземе worksheet write lock-а, и връща False с непокътната предишна селекция, ако нещо е недобре оформено

Защо HotXLS записва голяма селекция на листа като няколко BIFF8 Selection записа: таванът на тялото от 8224 байта побира 9 фиксирани байта плюс 1369 шестбайтови RefU области, така че 3000 области стават три записа от същия pane с 1369, 1369 и 262, а irefAct индексира агрегираната последователност, така че 1370 области с последната активна дават и на двата записа irefAct 1369
Всяко парче повтаря същата активна клетка и индекс, HotXLS reader-ът долепя поредните записи от същия pane в една група, а проверката за диапазон се пуска чак на EOF записа, когато цялата последователност е известна
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // header редът и колона A остават на мястото

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

    // Замразяването нулира съхранената селекция, затова селектирайте след замразяването.
    // 3000 области се записват като три Selection записа: 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;

Кой pane байт ползва Selection записът?

Selection записът идентифицира своя pane с числовия код, който форматът дефинира: 0 за долния десен, 1 за горния десен, 2 за долния ляв и 3 за горния ляв. Публичното изброяване TXLSPanePosition е декларирано в четивен ред — xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight — така че Ord(xlspTopLeft) е 0, което е долният десен pane във файла. Кастването на enum-а направо в pane байта би записало всяка горна лява селекция върху долния десен pane без никаква грешка. Всеки pane-aware входен пункт на HotXLS конвертира enum-а чрез изричен case statement вместо това, така че извикващите никога не се занимават с числовите кодове. Съществуването на pane също се проверява: горният десен pane съществува само при вертикален split, долният ляв само при хоризонтален, а долният десен само при двата. За pane, който текущата split или freeze геометрия няма, SelectAreas връща False, а GetSelectedAreas връща празен масив с ActiveAreaIndex -1, без да създава pane, selection обект или клетка в работната книга

Как HotXLS картира TXLSPanePosition върху BIFF8 Selection pane байта: enum-ът е деклариран в четивен ред, така че Ord(xlspTopLeft) е 0, докато файлът дефинира 0 за долен десен, 1 за горен десен, 2 за долен ляв и 3 за горен ляв, затова всеки pane-aware входен пункт конвертира чрез изричен case statement
Кастването на enum-а направо в pane байта би записало всяка горна лява селекция върху долния десен pane, затова HotXLS проверява и съществуването на pane спрямо текущата split или freeze геометрия, преди да пише

Къде живее скрол позицията на всеки pane?

Скрол позицията на всеки pane е разпределена в два записа, защото четири pane-а споделят само две редови и две колонови позиции. В класическа работна книга първият видим ред на горните pane-и и първата видима колона на левите pane-и са Window2.rwTop и Window2.colLeft, докато редът на долните pane-и и колоната на десните pane-и са Pane.rwTop и Pane.colLeft. ScrollWindow(xlspTopRight, R, C) затова записва Window2.rwTop и Pane.colLeft, а задаването на колоната на горния десен pane мести и долния десен pane, точно както двата споделят една хоризонтална scroll лента в Excel. Публичните методи ползват редове и колони номерирани от 1. Липсващ pane връща False и занулява и двата изхода на заявката, а координата извън диапазона се отхвърля, преди някоя ос да се е променила. Нищо тук не зависи от това как един viewer рисува мрежата. Rendering контролата държи собствени TopRow и LeftCol, както описва статията за рендериране на работни книги в персонализирана VCL мрежа, а това е runtime състояние, не това, което се записва

Къде живее всяка скрол ос на pane в HotXLS: четири pane-а споделят две редови и две колонови позиции, така че горният ред и лявата колона са Window2.rwTop и Window2.colLeft, докато долният ред и дясната колона са Pane.rwTop и Pane.colLeft, а ScrollWindow(xlspTopRight, 1, 6) записва едно Window2 поле плюс едно Pane поле и долният десен следва
XLSX разпределя същите данни върху sheetView и pane topLeftCell атрибутите, а сливането на двата слоя в един е точно начинът, по който горна или лява скрол позиция тихо изчезва при зареждане

XLSX разпределя същите данни върху два елемента: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) за прозореца като цяло и дъщерният pane/@topLeftCell (§18.3.1.66) за долно-десната страна на split. И двата атрибута могат да присъстват едновременно. HotXLS прочита външния атрибут първо в полетата на ниво прозорец, оставя pane детето да предефинира само полетата на ниво pane и записва и двете обратно поотделно. Сливането на двата слоя в един е точно начинът, по който горна или лява скрол позиция тихо изчезва при зареждане. Копията на лист носят и двата слоя и в двата engine-а. По-старите входни точки пазят първоначалното си поведение: класическите свойства ScrollRow и ScrollColumn и zero-based XLSX SetPaneScroll и GetPaneScroll. Самата freeze и split геометрия се настройва със настройките на ниво лист, разгледани в статията за защита на листове, page setup и печатане

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

  // Долен десен: долна редова ос (Pane.rwTop) и дясна колонова ос (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // Горен десен споделя дясната колонова ос, така че това мести и долния десен на колона 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]));
    // Долният десен започва от ред 500, колона 6
end;

Какво става, когато Selection запис е повреден?

Когато Selection запис е повреден, HotXLS го пази като opaque байтове, докладва диагностичен код 1304 (xlsDiagnosticSelectionRecordInvalid) и записва оригиналното тяло обратно байт по байт при save. Преди запис да се присъедини към групата на своя pane, reader-ът го проверява по реда. Pane байтът трябва да е 3 или по-малко. Записите за един pane трябва да са съседни в потока. 9-те фиксирани байта трябва да присъстват. cref трябва да е между 1 и 1369, а тялото трябва да е точно 9 + cref × 6 байта дълго. Всяко парче в група трябва да се съгласява по активната клетка и irefAct, irefAct не бива да има вдигнат знаков бит, активната колона трябва да е върху мрежата, и нито една област не бива да има обърнати граници. Проблемите в един физически запис се докладват по веднъж на запис. Противоречията, които изникват едва след агрегация, като irefAct, сочещ над общия брой области, или активна клетка извън индексираната област, се докладват по веднъж на група на EOF. Невалидна група остава невидима за типизирания API: GetSelectedAreas връща празен масив с индекс -1 за този pane, докато всеки друг pane продължава да работи

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;

Как селекциите оцеляват при вмъкване на редове и колони?

Селекциите оцеляват при структурни редакции, защото вмъкването или изтриването на цели редове или колони пренасочва всяка представена pane група и в двата engine-а, Classic и XLSX, през един споделен remapper. Оцелелите области пазят реда си, а активната област пази идентичността си. Ако активната област е изтрита, активна става първият оцелял наследник, а ако няма такъв — последният оцелял предшественик. Ако всички области са изтрити, групата се свива до една клетка на границата на изтриването, а активна клетка, която вече не попада в избраната област, се премества в горния ѝ ляв ъгъл, така че индексът и координатата никога не си противоречат. Лимитите са нарочни. Невалидни classic групи remapper-ът прескача, вместо да ги пренапише в измислена селекция, така че оригиналните им байтове продължават да минават през round-trip. Редакция на един pane сменя само записите на този pane и оставя другите byte-identical. ODS изобщо не получава pane selection състояние, защото ODF няма еквивалентна worksheet view структура, която да го носи

Ако вашето приложение пише .xls файлове, които потребителите отварят и трябва да разцъкват — да преглеждат маркирани клетки, да продължават оттам, откъдето са спрели, или да споделят замразен dashboard — pane-aware selection и scroll API-ят е част от HotXLS Delphi spreadsheet компонента и работи по същия начин за XLS и XLSX