Технічна стаття

Дублюйте аркуш XLSX у Delphi з HotXLS

Ви вже зібрали один аркуш саме так, як треба. Смуга заголовка об’єднана, ширини стовпців відповідають даним, верхні два рядки зафіксовані, область друку й поля налаштовані для чистого експорту в A4, а вкладка має колір, щоб фінансовий відділ міг її знайти. Тепер звіту потрібно дванадцять таких аркушів, по одному на кожен регіон, кожен із тією самою розкладкою. Якщо будувати цей аркуш у коді дванадцять разів, у дрібні відхилення легко просочується розсинхронізація: у регіону 7 стовпець стає на один пункт вужчим, у регіону 11 зникає фіксація, і ніхто цього не помічає, доки PDF не потрапить на стіл менеджера. Що вам насправді потрібно, так це програмна версія правого кліку в Excel, Move or Copy, Create a copy: взяти готовий аркуш і штампувати незалежні копії

Механізм XLSX у HotXLS, нативній бібліотеці Delphi та C++Builder, що читає й записує файли Excel без автоматизації самого Excel, уже вмів переміщувати аркуші, видаляти аркуші та копіювати діапазони клітинок між аркушами. Чого він не вмів до v2.91.0, так це клонувати цілий аркуш одним викликом. У цьому випуску з'являються дві точки входу: TXLSXWorksheet.CopyFrom, яка копіює стан рівня аркуша з одного аркуша на інший, і TXLSXSheets.Duplicate, яка додає новий аркуш і запускає CopyFrom для вас. Цікава частина не в тому, що вона щось копіює. Річ у навмисно проведеній межі між тим, що копіюється глибоко, і тим, що ні, та в тому, чому ця межа саме там

Один виклик, щоб клонувати готовий аркуш

Високорівнева операція тут така: DuplicateПередайте йому 1-based індекс вихідного аркуша, і він поверне абсолютно новий аркуш, що повторює розкладку та дані оригіналу. Домовленість щодо індексів збігається з Items[] на боці XLSX, тож перший аркуш має індекс 1, а не 0; передайте індекс поза діапазоном і отримаєте nil замість винятку, тобто той самий контракт на помилку, який використовує решта колекції аркушів XLSX

var
  Book: TXLSXWorkbook;
  Template, Copy: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Template := Book.Sheets.Add('Template');
    Template.Cells[1, 1].Value := 'Quarterly Statement';
    Template.Range['A1:C1'].Merge;
    Template.ColWidth[1] := 18;
    Template.FreezePanes(2, 1);          // freeze top row + first column
    Template.TabColorIsAuto := False;
    Template.TabColor := $FF1F4E79;

    // Clone with an explicit name...
    Copy := Book.Sheets.Duplicate(1, 'Region-North');
    // ...or let it pick the Excel-style default name.
    Copy := Book.Sheets.Duplicate(1);    // -> "Template (2)"

    Book.SaveAs('regions.xlsx');
  finally
    Book.Free;
  end;
end;

У цьому фрагменті варто сповільнитися на двох речах. По-перше, FreezePanes приймає аргументи по рядках спочатку, FreezePanes(ARow, ACol), тож це узгоджується з Cells[Row, Col] індексацією; дублікат успадковує точний поділ фіксації. По-друге, метод названо Duplicate і не більш очевидно Copy, і це не питання стилю. Copy є стандартною процедурою в System модулі, яку постійно використовують для рядків і динамічних масивів. Метод із назвою Copy у класі перекрив би її всередині тіл методів і створив би саме ту неоднозначність розв'язання, яка вдаряє вас через пів року. Duplicate обминає всю проблему і читається правильно в місці виклику

Назва за замовчуванням повторює власне правило Excel

Коли ви викликаєте одноаргументне перевантаження або передаєте порожній рядок імені, новий аркуш отримує назву на основі джерела з (2) суфіксом, а суфікс збільшується, доки назва не стане унікальною. Дублюйте Template аркуш один раз, і ви отримаєте Template (2); дублюйте його ще раз, і ви отримаєте Template (3), бо Template (2) уже зайнято. Це повторює імена, які Excel генерує власною командою Create a copy, тож книга, яку створює ваш код, виглядає саме так, як користувач очікував би від аркуша, скопійованого вручну. Перевірка унікальності працює щодо активної колекції аркушів, а значить, вона також перестрибує через імена, які ви створили вручну, не лише ті, що з'явилися після попередніх дублювань

Якщо ви генеруєте по одному аркушу на регіон або на місяць, краще скористайтеся перевантаженням із явною назвою. Передбачувана Region-North, Region-South схема легше адресувати пізніше, ніж низку (2), (3) суфіксів, і це зберігає читабельність ваших визначених імен та міжаркушових формул

Що CopyFrom копіює глибоко

Під капотом, Duplicate додає аркуш і потім викликає CopyFrom(ASource), який ви також можете викликати напряму, коли хочете клонувати на аркуш, який уже створили. CopyFrom одразу відсікає два вироджені випадки: копіювання з nil, або копіювання аркуша самого в себе, обидва варіанти негайно повертають керування і нічого не роблять. Далі йде власне копіювання, і воно навмисно охоплює багато всього

Дані клітинок ідуть першими. CopyFrom просить у джерела його UsedRange, тісну обмежувальну рамку заповнених клітинок і об'єднаних областей, і повторно використовує наявний механізм CopyRangeTo щоб перенести кожне значення, формулу і індекс стилю для кожної клітинки до цільового аркуша, починаючи з A1. Поверх клітинок він відтворює весь шар стану на рівні аркуша, який робить шаблон завершеним:

  • Об'єднані діапазони, відтворені за координатами, щоб банер займав той самий прямокутник
  • Ширина стовпців і висота рядків, плюс списки прихованих, згорнутих і рівнів контуру, скопійовані дослівно, щоб нестандартні рядки й стовпці збігалися точно
  • Закріплені області та стан перегляду: рівень масштабування, відображення сітки та нульових значень, напрямок справа наліво і тип подання
  • Стан захисту з його параметрами дозволів для кожної дії, щоб заблокований шаблон залишався заблокованим так само
  • Увесь блок параметрів сторінки: поля, орієнтація, розмір паперу, масштабування і вміщення на сторінку, область друку, заголовки друку, колонтитули, а також прапорці друку сітки й заголовків
  • Діапазон AutoFilter, колір вкладки та видимість аркуша

У результаті виходить аркуш, який друкується, фільтрується і подається так само, як джерело. А оскільки клітинки, об'єднання і списки розмірів фізично відтворюються на новому аркуші, а не спільно використовують ті самі об'єкти, дублікат повністю незалежний. Запишіть 999 у клітинку на копії, і джерело збереже своє початкове значення; ця незалежність - найважливіша властивість клону, призначеного для паралельних регіональних звітів, і поставлений у комплекті SheetCopy демо це прямо перевіряє

Що він залишає поверхневим, і чому

Тепер про чесну частину. Діаграми, вбудовані зображення, таблиці XLSX, перевірки даних і правила умовного форматування не копіюються. Це задокументована, навмисна межа, а не недогляд, і важливо розуміти причину, щоб планувати навколо неї, а не дивуватися потім

Кожна з цих колекцій має власну ідентичність і посилання, які не переживуть наївне копіювання полів. Діаграма вказує на вихідний діапазон даних і має власний зв'язок у пакеті OOXML; клонування об'єкта без переназначення зв'язку та посилань на ряди породжує діаграму, яка відмальовується за неправильними даними, або пакет, який Excel позначає як такий, що потребує відновлення. Таблиця має ім'я, яке має бути унікальним у межах книги, заголовковий рядок, прив'язаний до певних стовпців, і власний автоматично згенерований зв'язок. Умовне форматування і перевірки даних прив'язуються до діапазонів координат і, у випадку перевірки, можуть посилатися на інші діапазони через формулу. Глибоке копіювання будь-чого з цього означає переписування посилань і створення нових ідентичностей, а це вже справжня робота з реальними режимами відмов. Робити це наполовину, тобто копіювати об'єкт, але не його посилання, гірше, ніж не копіювати взагалі: це призводить до файла, що відкривається з підказкою про відновлення і мовчки відкидає вміст. Тому механізм копіює те, що може скопіювати чисто, а колекції з посиланнями залишає викликачеві, який знає, на що має вказувати ціль

На практиці це означає, що робочий процес для складнішого шаблону такий: дублюєте аркуш, щоб отримати клітинки, макет і налаштування друку, потім відтворюєте діаграму, таблицю, перевірки чи умовне форматування на копії з тим самим API, який ви використовували для створення їх уперше. Оскільки ви відтворюєте їх на власних діапазонах дубліката, посилання виходять правильними за самою побудовою. Для діаграми, що читає A1:C10, додайте нову діаграму на копії, що вказує на копію A1:C10; для живого AutoFilter, зверніть увагу, що фільтр діапазон зберігається, тож вам потрібно лише повторно застосувати критерії стовпців. Правила умовного форматування і перевірки даних, які ви б додали знову через ті самі виклики, описані в статті про об'єднані клітинки та компонування шаблону звіту, яка проходить по таблиці об'єднань і моделі діапазонів, успадкованих копією

Де дублювання стає частиною звітного конвеєра

Дублювання аркушів є природним доповненням до генерації на основі заповнювачів. Підхід, прив'язаний до токенів, у путівнику зі створення звітів на основі шаблонів у Delphi вирішує проблему внесення даних у макет, який редагують інші люди. Дублювання вирішує проблему, коли такий макет потрібен багато разів в одній книзі. Поєднайте ці підходи, і схема стане простою: тримайте один бездоганний Template аркуш із його токенами, об'єднаннями і налаштуванням друку, а потім для кожного регіону або періоду викликайте Duplicate, заповнюйте токени клону цим зрізом даних і рухайтеся далі. Недоторканий шаблон ніколи не змінюється, тож лишається надійним джерелом для наступного клону, а кожен вихідний аркуш починається з макета, однакового до байта

Одна заувага щодо послідовності позбавляє цілого класу плутанини. Дублюйте аркуш перед тим, як вносити в нього дані, а не після. Шаблон має зберігати структуру й форматування, а не цифри минулого кварталу, і клонування порожнього стилізованого аркуша означає, що кожен дублікат стартує чистим. Якщо ви дублюєте аркуш, який уже містить дані, ці дані також переходять далі, тому що CopyFrom копіює використаний діапазон точно; інколи саме це й потрібно, але для звіту з розгалуженням зазвичай ні

Швидка звичка перевірки

Оскільки різниця між глибоким і поверхневим копіюванням непомітна, доки її не шукати, додайте до завдання п'ятирядкову перевірку замість того, щоб просто довіряти, що все перенеслося. Після дублювання зчитайте назад структурні ознаки, які клон має успадкувати, і переконайтеся, що вони збігаються з джерелом

Copy := Book.Sheets.Duplicate(1, 'Region-North');
WriteLn(Format('merged=%d  colA=%.1f  freezeRow=%d  tabAuto=%d',
  [Copy.MergedCells.Count, Copy.ColWidth[1],
   Copy.FreezeRow, Integer(Copy.TabColorIsAuto)]));
// Prove independence: mutate the copy, confirm the source is untouched.
Copy.Cells[2, 2].Value := 999;
// Template.Cells[2, 2].Value is still whatever it was.

Кількість об'єднань, ширина стовпця, рядок фіксації та прапорець кольору вкладки показують, що шар, який було скопійовано, справді перенесено. Окремо, у будь-якому аркуші, де були діаграма, таблиця, перевірки даних або умовне форматування, вважайте їх списком на повторне створення в копії: їхня відсутність є задумом, а виправлення зводиться до кількох викликів, а не до повідомлення про помилку. Ця ментальна модель, глибоко там, де це безпечно, і поверхнево там, де посилання зламаються, і є вся суть того, як правильно користуватися цією функцією

Дублювання робочого аркуша та CopyFromкопію стану аркуша, описані тут, виходять у складі v2.91.0 нативного HotXLS Delphi spreadsheet component, разом із придатним до запуску SheetCopyзразком, що проходить цикл клонування й зміни від початку до кінця