HotXLS зберігає виділення аркуша й позиції прокрутки по областях через один API, який знає про області, і на TXLSWorksheet, і на TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow і TryGetWindowScroll. Для класичних файлів .xls HotXLS записує записи Selection BIFF8 (0x001D) щонайбільше по 1369 діапазонів кожен, перетворює логічні імена областей у байти областей, які визначає формат, і тримає кожну вісь прокрутки на тому записі Window2 чи Pane, де її чекає Excel
Проблема зазвичай вилазить у звіряльному чи аудиторському інструменті. Інструмент відкриває експорт реєстру, знаходить кожну клітинку, що розходиться з джерелом, і зберігає книгу з цими клітинками вже вибраними під закріпленою заголовною стрічкою, тож рецензент приземляється просто на розбіжності, а не гортає за ними. Сорок розбіжностей — і все гаразд. Файл на кінець місяця має 3 000, а одного запису Selection з 3 000 діапазонів не буває: його тіло вимагало б 18 009 байтів — удвічі більше за те, що несе один запис BIFF8. З позицією прокрутки пастка схожа. На аркуші із закріпленими областями «де дивився користувач» — це чотири області, які ділять між собою дві позиції рядків і дві позиції колонок, а не одна координата
Чому великому виділенню потрібно більше одного запису Selection?
Великому виділенню потрібно кілька записів, бо тіло запису BIFF8 обмежене 8224 байтами, а кожен вибраний діапазон коштує фіксовані шість байтів. [MS-XLS] §2.4.248 описує запис Selection як 9-байтову фіксовану частину (байт області, rwAct і colAct для активної клітинки, irefAct для активного діапазону і cref для кількості діапазонів), за якою йдуть cref структур RefU, кожна з двома 16-бітовими рядками і двома 8-бітовими колонками. Найбільша кількість, що вміщується, — це (8224 − 9) / 6 з округленням униз, тобто 1369, і це дає тіло на 8223 байти, на байт нижче межі. TXLSWorksheet.StoreSelectionGroup бере цю сталу як MaxAreasPerRecord і записує більшу групу як послідовні записи Selection тієї самої області, по 1369 діапазонів за раз
Неприємна деталь — це irefAct. Кожен шматок повторює той самий активний рядок, активну колонку й індекс активного діапазону, а irefAct індексує агреговану послідовність усіх шматків, а не діапазони всередині запису, який його несе. Виділення на один діапазон понад межу робить це конкретним: 1370 діапазонів з активним останнім стають двома записами, перший із cref 1369, другий із cref 1, і обидва несуть irefAct 1369. Це значення більше за власну кількість діапазонів другого запису. Читач, який звіряє irefAct із cref у кожному записі, відхилить валідний файл, а читач, який замінює свій стан на кожному записі, загубить перші 1369 діапазонів. Читач HotXLS додає послідовні записи тієї самої області в одну групу, вимагає, щоб кожен шматок згоджувався щодо активної клітинки й індексу, і запускає перевірку діапазону лише на EOF-записі аркуша, коли повна послідовність уже відома. Тому перевантаження SelectAreas, яке бере область першим аргументом, не має стелі в 1369 діапазонів. Воно звіряє кожне посилання A1 і активний індекс, перш ніж захопити блокування аркуша на зміну, і повертає False з недоторканим попереднім виділенням, якщо щось сформовано криво
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
Diffs: TXLSSelectedAreas;
I: Integer;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add;
Sheet.FreezePanes(1, 1); // заголовний рядок і колонка 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;
Який байт області використовує запис Selection?
Запис Selection ідентифікує свою область числовим кодом, який визначає формат: 0 для нижньої правої, 1 для верхньої правої, 2 для нижньої лівої і 3 для верхньої лівої. Публічний перелік TXLSPanePosition оголошено в порядку читання — xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, — тож Ord(xlspTopLeft) дорівнює 0, а це в файлі нижня права область. Пряме приведення enum до байта області записало б кожне виділення верхньої лівої області в нижню праву без жодної помилки. Кожен точ входу HotXLS, що знає про області, перетворює enum через явний case, тож викликачі взагалі не мають справи з числовими кодами. Перевіряється й існування області: верхня права існує лише за вертикального розбиття, нижня ліва — лише за горизонтального, а нижня права — лише за обох. Для області, якої не має поточна геометрія розбиття чи закріплення, SelectAreas повертає False, а GetSelectedAreas повертає порожній масив із ActiveAreaIndex -1, не створюючи ані області, ані об'єкта виділення, ані клітинки в книзі
Де живе позиція прокрутки кожної області?
Позиція прокрутки кожної області розкладена на два записи, бо чотири області ділять лише дві позиції рядків і дві позиції колонок. У класичній книзі перший видимий рядок верхніх областей і перша видима колонка лівих — це Window2.rwTop і Window2.colLeft, а рядок нижніх областей і колонка правих — це Pane.rwTop і Pane.colLeft. Тож ScrollWindow(xlspTopRight, R, C) пише Window2.rwTop і Pane.colLeft, і виставлення колонки верхньої правої області пересуває також нижню праву — так само, як обидві ділять один горизонтальний scrollbar в Excel. Публічні методи використовують номери рядків і колонок від одиниці. Відсутня область повертає False і виставляє обидва вихідні параметри запиту в нуль, а координата поза діапазоном відхиляється до того, як хоч одна вісь зміниться. Тут ніщо не залежить від того, як глядач малює сітку. Контрол рендерингу тримає власні TopRow і LeftCol, як описує стаття про рендеринг книг у власному VCL grid, і це стан часу виконання, а не те, що зберігається
XLSX розкладає ті самі дані по двох елементах: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) для вікна загалом і дочірній pane/@topLeftCell (§18.3.1.66) для нижньої правої сторони розбиття. Обидва атрибути можуть бути присутні одночасно. HotXLS читає зовнішній атрибут першим у поля рівня вікна, дозволяє дочірньому pane перевизначити лише поля рівня області і записує обидва назад окремо. Злиття двох шарів в один — це рівно те, як позиція прокрутки верхньої чи лівої області мовчки зникає при завантаженні. Копії аркушів несуть обидва шари в обох рушіях. Старіші точки входу тримають свою початкову поведінку: класичні властивості ScrollRow і ScrollColumn, а також нуль-індексовані XLSX SetPaneScroll і GetPaneScroll. Саму геометрію закріплення й розбиття налаштовують параметри рівня аркуша, про які йдеться в захисті аркуша, налаштуванні сторінки й друку
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 тримає його як непрозорі байти, повідомляє діагностичний код 1304 (xlsDiagnosticSelectionRecordInvalid) і при збереженні повертає початкове тіло байт у байт. Перш ніж запис приєднається до групи своєї області, читач перевіряє його по порядку. Байт області мусить бути 3 чи менше. Записи однієї області мусять іти в потоці безперервно. Дев'ять фіксованих байтів мають бути на місці. cref мусить бути між 1 і 1369, а тіло — мати рівно 9 + cref × 6 байтів завдовжки. Кожен шматок у групі мусить згоджуватися щодо активної клітинки й irefAct, знаковий біт irefAct мусить бути скинутий, активна колонка мусить бути на сітці, і жоден діапазон не може мати перекинутих меж. Проблеми всередині одного фізичного запису повідомляються раз на запис. Суперечності, які видно лише після агрегації, — як-от irefAct поза загальною кількістю діапазонів чи активна клітинка поза проіндексованим діапазоном — повідомляються раз на групу на EOF. Невалідна група лишається невидимою для типізованого API: GetSelectedAreas повертає для цієї області порожній масив з індексом -1, тоді як усі інші області працюють далі
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;
Як виділення переживають вставляння рядків і колонок?
Виділення переживають структурні правки, бо вставляння чи видалення цілих рядків і колонок перемаплює кожну представлену групу областей в обох рушіях, Classic і XLSX, через один спільний перемаплювач. Діапазони, що вижили, тримають свій порядок, а активний діапазон — свою ідентичність. Якщо активний діапазон видалено, активним стає перший виживший наступник, а якщо за ним нічого немає — останній виживший попередник. Якщо видалено всі діапазони, група згортається в одну клітинку на межі видалення, а активна клітинка, що вже не падає всередину вибраного діапазону, зсувається до верхнього лівого кута цього діапазону, тож індекс і координата ніколи не суперечать одне одному. Межі тут навмисні. Невалідні класичні групи перемаплювач пропускає, а не переписує в вигадане виділення, тож їхні початкові байти все ще проходять туди й назад. Редагування однієї області замінює лише записи цієї області й лишає інші байт-у-байт. ODS взагалі не отримує стану виділення по областях, бо ODF не має еквівалентної структури перегляду аркуша, яка могла б його нести
Якщо ваш застосунок пише файли .xls, які користувачі відкривають і мають гортати — чи то рецензувати позначені клітинки, чи продовжити з місця зупинки, чи поділитися закріпленим дашбордом, — API виділення й прокрутки, що знає про області, входить у компонент електронних таблиць HotXLS Delphi і працює однаково для XLS і XLSX