HotXLS хранит выделения листа и позиции прокрутки в разрезе панелей через один API с учётом панелей — SelectAreas, GetSelectedAreas, ScrollWindow и TryGetWindowScroll на обоих классах, TXLSWorksheet и TXLSXWorksheet. Для классических файлов .xls HotXLS пишет записи BIFF8 Selection (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, а это в файле правая нижняя панель. Лобовое приведение перечисления к байту панели записало бы каждое выделение левой верхней на правую нижнюю панель — без единой ошибки. Каждый панельный вход HotXLS конвертирует перечисление через явный case, так что вызывающим вообще не приходится иметь дело с числовыми кодами. Проверяется и существование панели: правая верхняя существует только при вертикальном разделении, левая нижняя — только при горизонтальном, а правая нижняя — только при обоих. Для панели, которой нет в текущей геометрии разделения или закрепления, SelectAreas возвращает False, а GetSelectedAreas — пустой массив с ActiveAreaIndex равным -1, не создавая ни панели, ни объекта выделения, ни ячейки в книге
Где живёт позиция прокрутки каждой панели?
Позиция прокрутки каждой панели размазана по двум записям, потому что четыре панели делят лишь две позиции строк и две позиции столбцов. В классической книге первая видимая строка верхних панелей и первый видимый столбец левых панелей — это Window2.rwTop и Window2.colLeft, а строка нижних панелей и столбец правых — Pane.rwTop и Pane.colLeft. Поэтому ScrollWindow(xlspTopRight, R, C) пишет Window2.rwTop и Pane.colLeft, и установка столбца правой верхней панели двигает и правую нижнюю — ровно как эти двое делят одну горизонтальную полосу прокрутки в Excel. Публичные методы используют нумерацию строк и столбцов с единицы. Отсутствующая панель возвращает False и обнуляет оба выходных параметра запроса, а координата вне диапазона отвергается до того, как сдвинется хоть одна ось. Здесь ничто не зависит от того, как вьюер рисует сетку. Рендер-контрол держит собственные TopRow и LeftCol, как описывает статья про отрисовку книг в собственном VCL-гриде, и это состояние времени выполнения, а не то, что сохраняется
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. Записи одной панели должны быть смежными в потоке. 9 фиксированных байтов должны присутствовать. 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;
Как выделения переживают вставку строк и столбцов?
Выделения переживают структурные правки, потому что вставка или удаление целых строк или столбцов перемапливает каждую представленную панельную группу в обоих движках, классическом и XLSX, через один общий ремаппер. Пережившие области сохраняют порядок, а активная область — свою идентичность. Если активная область удалена, активной становится первая пережившая преемница, а если за ней ничего нет — последняя пережившая предшественница. Если удалены все области, группа схлопывается в одну ячейку на границе удаления, а активная ячейка, которая больше не попадает внутрь выбранной области, переезжает в её левый верхний угол — индекс и координата никогда не противоречат друг другу. Ограничения намеренные. Невалидные классические группы ремаппер пропускает, а не переписывает в выдуманное выделение, так что их исходные байты всё ещё проходят round-trip. Правка одной панели заменяет записи только этой панели, а остальные оставляет байт в байт. ODS состояния выделения панелей не получает вовсе, потому что в ODF нет эквивалентной структуры представления листа, которая могла бы его нести
Если ваше приложение пишет файлы .xls, которые пользователи потом открывают и по которым им нужно перемещаться — просмотреть помеченные ячейки, продолжить с того места, где остановились, или расшарить закреплённый дашборд, — API выделения и прокрутки с учётом панелей входит в состав компонента электронных таблиц HotXLS для Delphi и одинаково работает для XLS и XLSX