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

HotXLS: protection, page setup, and printing в Delphi

Три группы настроек рабочего листа не имеют никакого отношения к значениям ячеек и полностью определяют, как файл будет вести себя после того, как покинет ваш код. Защита листа решает, какие ячейки пользователь может редактировать после того, как вы передадите книгу. Параметры страницы задают ориентацию, размер бумаги и поля. Настройки печати (повторяющиеся строки заголовков, масштабирование и ручные разрывы страниц) управляют тем, как сетка произвольной длины ляжет на бумагу. Ни одна из этих трёх групп не видна, когда вы просто просматриваете данные в средстве просмотра, и все три тихо ломаются на практике, если заданы неправильно. HotXLS, нативная библиотека для электронных таблиц для Delphi и C++Builder, предоставляет полную поверхность API для .xls и .xlsx, а значит, воспроизводит и каждое нелогичное на первый взгляд правило Excel, встроенное в эту поверхность

Первое из этих правил сбивает с толку почти всех, кто впервые защищает сгенерированный лист. Вызовите Protect — и внезапно никто не может ввести значение ни в одну ячейку, включая столбцы для ввода данных, вокруг которых вы построили книгу. Ваш код ни разу не коснулся этих столбцов, и именно поэтому так происходит

Каждая ячейка рождается заблокированной

ECMA-376 определяет locked как часть записи форматирования ячейки, а не как свойство самой защиты, и по умолчанию это значение равно true. Защита листа — это лишь переключатель, который заставляет флаг действовать. Поэтому вся сетка несёт флаг блокировки с момента своего появления — спящий, — а вызов Protect активирует их все разом. Решение — задать порядок действий осознанно: сначала построить макет, явно снять блокировку с диапазонов, которые должны редактировать пользователи, и только в конце защитить лист

Диаграмма порядка защиты HotXLS в Delphi: каждая ячейка рождается с locked true, диапазоны ввода разблокируются SetLocked первыми, а Sheet.Protect, вызванный последним, держит разблокированные ячейки редактируемыми
Ячейки приходят заблокированными по умолчанию, поэтому сперва разблокируйте диапазоны ввода, а Protect вызывайте последним, чтобы они остались редактируемыми
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Timesheet');
  // ... здесь записаны строка заголовка, столбец имён и формулы ставок ...
  Sheet.Range['B2:B50'].SetLocked(False);         // сюда сотрудники вводят часы
  Sheet.Range['F2:F50'].SetFormulaHidden(True);   // скрыть расчёт ставок
  Sheet.Protect('review-2026');                   // теперь флаги блокировки вступают в силу
  Book.SaveAs('timesheet.xlsx');
finally
  Book.Free;
end;

SetFormulaHidden делает нечто отдельное и легко упускаемое из виду: пока защита активна, ячейка по-прежнему показывает вычисленное значение, но строка формул остаётся пустой. Это важно, когда формула содержит тарифы, маржу или весовые коэффициенты, которые вы не хотели бы отдавать каждому получателю, кликнувшему по итогу. На фасаде XLS то же самое намерение выражается для каждого диапазона через IXLSRange.Locked и FormulaHidden. Там же рабочий лист несёт пятнадцать флагов Allow* (AllowSort, AllowAutoFilter, AllowFormatCells и остальные), так что защищённый лист по-прежнему можно сортировать и фильтровать, а не замораживать в виде запечатанного экспоната

От чего на самом деле защищает пароль защиты

Оба формата хранят пароль защиты листа и книги как устаревший хеш из 4 шестнадцатеричных цифр. Шестнадцать бит означают, что бесчисленное множество строк совпадает с хешем любого заданного пароля, а инструменты для снятия защиты — на расстоянии одного поискового запроса. Относитесь к защите как к ремню безопасности от случайных правок, а не как к контролю доступа. Это правильный инструмент, чтобы удержать рецензентов от ввода поверх столбца с формулами, и неправильный — для всего, что связано со словом «конфиденциально»

Уровнем выше ProtectWorkbook на фасаде XLSX блокирует структуру книги, что запрещает добавление, переименование, удаление или изменение порядка листов. Задавайте это, когда сам список листов является контрактом с нижестоящим парсером, индексирующим листы по имени или позиции. Переименованный лист ломает импорт на другой стороне так же надёжно, как и удалённый столбец. Фасад XLS отражает ту же слоистость через TXLSWorkbook.Protect на уровне книги и вызовы Protect для каждого листа, плюс свойство isProtected для кода, которому нужно проверить унаследованный файл перед тем, как что-либо в нём менять

Когда требуется настоящая конфиденциальность, механизм меняется полностью. SaveAsEncrypted создаёт AES-зашифрованный пакет по схеме ECMA-376 Standard Encryption, подробно описанной в руководстве по AES-защищённому выводу XLSX, а устаревший фасад XLS записывает и читает RC4-зашифрованные файлы .xls через EncryptionPassword и перегрузку Open с паролем. Это различие не академическое. Защищённый лист перемещается в открытом виде, поэтому любой zip-инструмент может прочитать значения его ячеек, тогда как зашифрованный пакет нечитаем без пароля. Строка аудита с формулировкой «файл с зарплатной ведомостью должен быть защищён» почти всегда подразумевает именно шифрование, каким бы словарём при этом ни пользовались

Диаграмма контраста: защита листа HotXLS, хранящая 16-битный прежний хеш и оставляющая значения ячеек открытым текстом, читаемым любым zip-инструментом, против вывода SaveAsEncrypted с AES, остающегося нечитаемым без пароля
Защита листа — ремень безопасности против случайных правок при читаемом открытом тексте, и только AES-шифрование прячет содержимое

Параметры страницы — часть контракта документа

Поведение при печати невидимо на экране, поэтому оно так часто уходит в продакшн сломанным. В тот момент, когда заказчик распечатывает книгу или экспортирует её в PDF для аудитора, поля, масштабирование и повторяющиеся заголовки превращаются в функциональные требования, которые никто не тестировал. На фасаде XLSX эти настройки висят прямо на рабочем листе:

Sheet.PageLandscape := True;
Sheet.PaperSize := xlsxPaperA4;
Sheet.SetPageMargins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3);
Sheet.CenterHeader := 'Monthly Timesheet';
Sheet.RightFooter := 'Page &P of &N';
Sheet.PrintArea := '$A$1:$F$60';     // голая ссылка: без имени листа
Sheet.PrintTitleRows := '$1:$1';     // строка заголовка повторяется на каждой странице
Sheet.FitToWidth := 1;
Sheet.FitToHeight := 0;              // расти вниз по мере роста данных
Sheet.PrintGridlines := False;

Две из этих строк скрывают ловушки. Строки заголовка и подвала используют коды форматирования Excel: &P для текущей страницы, &N для общего количества, а &L, &C и &R — чтобы явно адресовать три секции. Другая ловушка — PrintArea, которая намеренно принимает голую ссылку на ячейки. HotXLS хранит её без квалификации и добавляет имя листа в качестве префикса при записи файла, поэтому если вы сами передадите 'Timesheet!$A$1:$F$60', получится дважды квалифицированная, некорректная ссылка. Та же осторожность нужна на уровень ниже: области печати и заголовки печати сохраняются как встроенные именованные диапазоны _xlnm.Print_Area и _xlnm.Print_Titles, поэтому никогда не добавляйте записи _xlnm.* через DefinedNames вручную — иначе оба механизма начнут конфликтовать за один и тот же слот

Масштабирование, которое выдерживает продакшн-объёмы данных

Сочетание FitToWidth := 1 с FitToHeight := 0 читается как «всегда умещать столбцы на одну страницу по ширине, а затем брать столько страниц вниз, сколько потребуют данные», и это правильное значение по умолчанию для любого отчёта с переменным числом строк. Ловушка — настраивать фиксированный процент или пару «уместить на странице» по тестовому файлу из тридцати строк: подайте те же настройки на шестьсот продакшн-строк, и результат либо разлетится на десятки обрезанных страниц, либо сожмётся ниже уровня читаемости. Масштабируйте ширину, позволяйте длине расти и повторяйте строку заголовка через PrintTitleRows, чтобы семнадцатая страница оставалась читаемой сама по себе

Диаграмма масштабирования печати HotXLS в Delphi: FitToWidth равен 1, так что каждая страница остаётся шириной в один лист; FitToHeight равен 0, так что страницы растут вниз; PrintTitleRows повторяет полосу заголовка; разрывы страниц регенерируются после ClearAllPageBreaks
FitToWidth 1 при FitToHeight 0 держит каждую страницу шириной в один лист, тогда как повторяемые строки заголовков и регенерированные разрывы хранят читабельность

Ручные разрывы страниц подчиняются той же дисциплине перегенерации, что и всё остальное в сгенерированной книге. AddRowBreak(BeforeRow) начинает новую страницу перед границей раздела, но когда генератор перезапускается и строки смещаются, устаревший разрыв оказывается посреди таблицы. Сначала вызывайте ClearAllPageBreaks, а затем заново добавляйте разрывы, вычисленные из собственных счётчиков строк генератора, а не латайте старые позиции. На фасаде XLS эквивалентные настройки живут в Sheet.PageSetup (ориентация, размер бумаги, поля, строки заголовка и подвала, вписывание в страницы), а RepeatRows и RepeatColumns отвечают за заголовки печати

Проверка результата раньше, чем это сделает заказчик

У ошибок защиты и печати есть общее свойство: их тривиально проверить вручную, и их почти никогда не проверяют. Откройте сгенерированный файл в Excel и потратьте на него девяносто секунд. Введите значение в ячейку для ввода данных и убедитесь, что она принимает нажатие клавиш; введите значение в заблокированную ячейку и убедитесь, что появляется запрос на снятие защиты; проверьте, что скрытая формула оставляет строку формул пустой. Затем запустите предпросмотр печати на наборе данных продакшн-размера, а не на выборке из тридцати строк, и посмотрите количество страниц, повторяющуюся строку заголовка и нумерацию в подвале. Предпросмотр — это шаг, который окупает себя, потому что геометрия печати зависит от настроек, никак не отображаемых на экране, и, если не считать физический принтер, это единственное место, где ошибка масштабирования вообще становится видимой

Ещё одна настройка завершает проверку. FreezePane(ACol, ARow) удерживает блок заголовков в поле зрения, пока рецензент прокручивает лист. Это поведение экрана, а не печати, но рецензент оценивает весь результат целиком. А книга, которая начинает жизнь как макет, поддерживаемый дизайнером, получает большую часть этого бесплатно: рабочий процесс генерации отчётов по шаблону хранит параметры страницы в шаблоне, где их настроил человек по реальному принтеру, оставляя коду лишь заполнение данных и повторное применение защиты после того, как макет устоялся

HotXLS — нативная библиотека Object Pascal для работы с электронными таблицами для Delphi и C++Builder; полный справочник API по защите и параметрам страницы находится на странице продукта HotXLS Delphi Component