HotXLS, рідна бібліотека Excel для Delphi та C++Builder, створена для безвтратних кругових шляхів XLSX: відкрийте робочу книгу, змініть одну клітинку, збережіть — і користувацька тема, сторонні блоки розширення extLst та ланцюжок обчислень збережуться. За це відповідають три механізми — дослівне кешування xl/theme/theme1.xml, повторна серіалізація невідомих блоків <ext> на основі подій та створення нового, сумісного зі специфікацією файлу xl/calcChain.xml при кожному збереженні робочої книги з формулами
Сценарій, який мотивує всі три механізми, є надзвичайно поширеним. Служба виставлення рахунків завантажує шаблон, розроблений клієнтом в Excel — корпоративна колірна тема, спарклайни в стовпці KPI, правило умовного форматування, додане новішою збіркою Excel — записує одну суму рахунка-фактури в клітинку B3 і зберігає. Клієнт відкриває результат, а фірмові кольори повернулися до стандартного синього кольору Office, спарклайни зникли, а Excel пропонує «відновити» файл. При цьому ніщо в коді не торкалося жодної з цих функцій. Бібліотека зробила це просто шляхом збереження
Чому файли Excel втрачають форматування після редагування бібліотекою?
Файли Excel втрачають форматування після редагування бібліотекою, оскільки більшість бібліотек не редагують файл — вони перебудовують його. Пакет .xlsx — це ZIP-архів XML-частин: xl/workbook.xml, по одному xl/worksheets/sheetN.xml на аркуш, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml тощо. Типова бібліотека розбирає ці частини в модель об'єктів під час відкриття та регенерує кожну частину з цієї моделі при збереженні. Будь-яка функція, яку модель не представляє — тема, яку вона ніколи не розбирала, або блок розширення з новішого Excel — не має місця в пам'яті, тому регенерована частина мовчки опускає її
Стандарт ECMA-376 передбачав половину цієї проблеми. Специфікація SpreadsheetML визначає extLst (ECMA-376 Part 1, «Область зберігання даних майбутніх функцій», §18.2.10 для елемента рівня книги) як визначену точку розширення: новіші генератори розміщують там функції, кожна з яких загорнута в елемент <ext> із атрибутом uri, який ідентифікує функцію, а старіші споживачі повинні зберігати те, чого вони не розуміють. Спарклайни, зрізи та новіші типи умовного форматування передаються саме так. Бібліотека, яка відкидає невідомі блоки <ext>, є не просто такою, що втрачає дані — вона порушує контракт прямої сумісності, навколо якого створювався формат. Питання до будь-якої бібліотеки електронних таблиць, яку ви оцінюєте, є прямим: якщо я зміню одну клітинку, що ще зміниться
Як HotXLS зберігає користувацьку тему байт-у-байт?
HotXLS зберігає тему робочої книги шляхом кешування оригінальних байтів xl/theme/theme1.xml під час відкриття та їх дослівного запису назад при збереженні. Частина теми (ECMA-376 Part 1, §14.2.7) — це DrawingML, а не SpreadsheetML (схеми кольорів, шрифтів, форматів), і рушій електронних таблиць не має причин глибоко моделювати її. Попередні версії HotXLS генерували фіксовану тему Office при кожному збереженні, що є саме тією помилкою «повернення до фірмових кольорів», про яку йшлося вище; починаючи з версії v2.89.46, тема відкритого пакета зберігається в сирому вигляді та повторно виводиться недоторканою, а вбудована тема Office генерується лише для робочих книг, створених з нуля. Сирі байти — це найсильніша гарантія точності: немає розбору, немає повторної серіалізації, немає шансів на відхилення
Дослівна копія навмисно виграє у програмного доступу до теми. Клас TXLSXWorkbook надає властивості ThemeMajorFont та ThemeMinorFont, щоб ви могли вибирати шрифти заголовків та тіла для нових робочих книг, але коли під час відкриття було зафіксовано дослівну тему, ці методи встановлення не впливають на збережений файл — круговий шлях має пріоритет. Якщо вам дійсно потрібно змінити тему існуючої робочої книги, це є сигналом для редагування шаблону в самому Excel, а не через орієнтований на дані API. Повсякденний випадок взагалі не потребує API
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('branded-invoice.xlsx');
Book.Sheets[0].Cells[3, 2].Value := 42750.00; // the one edit
Book.SaveAs('branded-invoice-out.xlsx');
// theme1.xml in the output is byte-identical to the input
finally
Book.Free;
end;
end;
Що відбувається з невідомими блоками extLst при збереженні?
HotXLS захоплює кожен блок <ext> на рівні робочого аркуша, який він не моделює вбудовано, і відтворює його в extLst збереженого аркуша, тому функції, записані новішими збірками Excel, виживають після кругового шляху недоторканими. Починаючи з версії v2.131.0, захоплені фрагменти видно через властивість лише для читання RawWorksheetExts, яка є TStringList на кожному аркуші XLSX, що робить гарантію перевіряємою з тестового коду, а не предметом віри
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
i: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('from-newer-excel.xlsx');
Sheet := Book.Sheets[0];
WriteLn(Format('%d foreign ext block(s) captured',
[Sheet.RawWorksheetExts.Count]));
for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
finally
Book.Free;
end;
end;
Деталь реалізації, яку варто знати, полягає в тому, що захоплення — це повторна серіалізація на рівні подій, а не копіювання сирих байтів. Потоковий XML-зчитувач HotXLS не надає вихідних зсувів, тому невідоме піддерево перебудовується з подій Element, Text та EndElement у міру їх проходження. Цей підхід приховує одну класичну пастку: самозакривний елемент на кшталт <a/> викликає лише подію Element, позначену як порожня, і ніколи не викликає EndElement, тому будь-який лічильник глибини, який зменшується виключно за подією EndElement, ніколи не побачить закриття піддерева. Обробіть це, і перебудований фрагмент буде семантично еквівалентним оригінальному — лапки атрибутів та самозакривні форми нормалізуються, тому він не є байт-ідентичним, але Excel зчитує зміст, а не байти. Дві властивості власного виводу Excel роблять відтворення безпечним: Excel оголошує необхідні атрибути xmlns на елементі <ext> або всередині нього, тому кожен захоплений фрагмент є самодостатнім щодо простору імен, і саме ця самодостатність дозволяє дублювати аркуш у межах або між робочими книгами, переносячи сторонні блоки звичайним призначенням списку рядків
Запис calcChain.xml для довіри Excel до ваших формул
HotXLS пише xl/calcChain.xml (частина ланцюжка обчислень, ECMA-376 Part 1, §12.3.1) щоразу, коли збережена робоча книга містить формули, і він вибирає між двома впорядкуваннями. Якщо граф залежностей формул уже побудований і є актуальним — ви викликали Recalculate після останнього редагування — ланцюжок виводиться в повному топологічному порядку (залежності перед залежними), а всі учасники циклічних посилань додаються в кінці. В іншому випадку клітинки перелічуються в порядку документа. Обидва варіанти є правильними: примітки до реалізації формату від Microsoft, [MS-XLSX], розглядають ланцюжок обчислень як підказку, яку Excel перевіряє та впорядковує під час завантаження, тому будь-який повний перелік є легальним, і HotXLS навмисно відмовляється примусово будувати граф всередині SaveAs — побудова ребер є квадратичною від кількості клітинок, що є неприйнятною прихованою вартістю при збереженні мільйона клітинок
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');
Чому варто дбати про частину, яку Excel розглядає як рекомендаційну? Тому що її відсутність є сигналом. Деякі споживачі — евристика відновлення, сторонні переглядачі, інструменти порівняння — очікують, що робоча книга з формулами міститиме ланцюжок обчислень, і бібліотека, яка мовчки опускає цю частину при збереженні, створює файли, які дещо відрізняються від того, що пише Excel. Вивід коректного ланцюжка тримає результат у межах того, на чому була протестовата решта екосистеми, що є тихою, непублічною основою кругової інженерії
Де закінчується безвтратний круговий шлях
Чесність тут важливіша за маркетингову позначку, тому межі заслуговують на однакову увагу. HotXLS не копіює весь пакет байт-у-байт: XML аркуша, стилі, спільні рядки та частини робочої книги регенеруються з розібраної моделі, тому вихідні дані є семантично точними, але не бінарно-ідентичними — локальні заголовки ZIP містять свіжі часові мітки DOS. Захоплені фрагменти <ext> повертаються нормалізованими, як описано вище. Програмні перевизначення шрифтів теми ігноруються, коли присутня дослівна тема. Мережа збереження має визначений розмір: функції, які HotXLS моделює вбудовано (наприклад, спарклайни розбираються і перезаписуються, а не сліпо копіюються), плюс сторонній вміст extLst, плюс дослівно кешовані частини. Частина, яка не моделюється і не знаходиться всередині точки розширення (наприклад, спеціальна частина екзотичної надбудови), випадає з трьох механізмів, які описує ця стаття, тому тестуйте свої реальні шаблони, а не припускайте
Супутня робота зі збереження доповнює картину. Проекти VBA та посилання на зовнішні робочі книги проходять через збереження за тим самим принципом «зберегти те, що не моделюється», описаним у супутній статті про збереження VBA та зовнішніх посилань, а властивості документа в docProps мають власний API читання-запису замість того, щоб мовчки опускатися. Коли ви оцінюєте будь-яку бібліотеку електронних таблиць, запустіть тест однієї клітинки: відкрийте багату на функції виробничу робочу книгу, змініть одне значення, збережіть і порівняйте розпаковані частини з оригіналом. Те, що змінилося за межами аркуша, який ви чіпали, розповість вам про бібліотеку більше, ніж будь-яка матриця функцій
Механізми кругового шляху, описані тут — дослівне збереження теми починаючи з версії v2.89.46, захоплення сторонніх extLst та вивід calcChain.xml починаючи з версії v2.131.0 — постачаються в поточному компоненті HotXLS Delphi Excel Component, сторінка продукту якого документує повний набір функції читання та запису XLSX для Delphi та C++Builder