Проітеруйте клітинки щойно відкритого шаблону звіту — і об’єднаний заголовок поводитиметься як витік. Ви читаєте A1 й отримуєте «Quarterly Statement»; читаєте B1 до F1, які видимо сидять під тим самим банером, і не отримуєте нічого. Запишіть значення в C1, щоб полагодити шапку, — і воно ніколи не з’явиться на екрані. Сітка не загубила ваших даних. Вона робить рівно те, що означає об’єднання: і в XLS, і в XLSX об’єднаний прямокутник промальовує вміст однієї клітинки, верхнього лівого якоря, а решту вважає покритим простором, що тримає значення, але ніколи їх не показує. Користувачі Excel засвоюють це методом спроб і помилок. Генератор звітів мусить закодувати це як правило, бо в згенерованому коді симптомом є порожня область без жодного винятку, за яким її можна відстежити. HotXLS, нативна бібліотека на Object Pascal, що читає й пише обидва формати Excel з Delphi та C++Builder, оприлюднює таблицю об’єднань достатньо явно, щоб ви програмували проти цього правила, а не перевідкривали його у зверненні до підтримки
Одне значення, один якір
Об’єднання — це вказівка щодо відображення, накладена на сітку, яка форми не змінює. Кожна покрита клітинка й далі існує у файлі як власний слот; запис об’єднання лише каже споживачеві промалювати вміст якоря по всьому прямокутнику. Ця відмінність спричиняє три поведінки, які варто засвоїти, перш ніж писати бодай рядок коду компонування. Читання покритої клітинки повертає її власне збережене значення, а для банера, який ви побудували, воно зазвичай порожнє, тож будь-який код, що оглядає об’єднаний заголовок, мусить розв’язати якір і прочитати саме його. Запис у покриту клітинку вдається на рівні файлу й не показується ніде — це і є пастка з невидимою шапкою з початку статті. А роз’єднання області оголює те, що весь цей час під нею сиділо, тож випадкове значення, записане в покритий простір, обертається на видимий дефект того дня, коли хтось розчинить об’єднання
З боку XLSX ця таблиця є повноцінним об’єктом. Sheet.MergedCells несе Add('A1:C1'), FindAt(Row, Col), DeleteAt та Items, а виклик, до якого тягнешся найчастіше, — це FindAt: передайте йому будь-яку координату, і він поверне об’єднану область, що покриває цю клітинку, або nil, коли клітинка стоїть сама. Цей єдиний пошук — підмурівок обох половин правильного поводження з об’єднаннями: безпечного читання й охорони запису, і обидві з’являться далі
Два фасади, дві ідіоми об’єднання
HotXLS тримає класичний рушій BIFF8 для .xls і рушій OOXML для .xlsx як окремі об’єктні моделі, і об’єднання вони пишуть по-різному, бо походять від різних домовленостей. Фасад XLS дотримується ідіоми Excel COM: ви берете діапазон із двоаргументної індексованої властивості й викликаєте Merge з OleVariant, чиє значення вирішує, яку геометрію ви отримаєте
var
Book: IXLSWorkbook; // з підрахунком посилань: без ручного Free
Sh: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
Sh := Book.Sheets[1]; // колекція аркушів XLS нумерується з одиниці
Sh.Range['A1', 'F1'].Merge(False); // False = один об’єднаний блок
Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
Sh.Range['A3', 'F4'].Merge(True); // True = об’єднати впоперек: одне об’єднання на рядок
Book.SaveAs('layout.xls');
end;
Аргумент до Merge — це та частина, у якій люди помиляються. Над діапазоном із двох рядків Merge(True) дає два незалежні однорядкові об’єднання, тобто «Об’єднати впоперек» в Excel і рівно те, що потрібно для багатоярусної смуги заголовків, чиї рядки мають лишатися роздільними. Merge(False) зливає весь прямокутник в один блок. Діапазон також повідомляє MergeCells як прапорець стану, повертає охопну область через MergeArea й розчиняє себе через Unmerge. Фасад XLSX відкриває ті самі операції під іншими назвами: Sheet.MergeCells(Row1, Col1, Row2, Col2) бере цілочислові межі, TXLSXRange.Merge приймає еквівалентний варіант Across, а результат тримає колекція MergedCells
Шаблон, що росте разом зі своїми даними
Справжній шаблон звіту — не фіксована сітка. Шапка й підсумки фіксовані, а секція деталей між ними розтягується до того, що поверне запит. Підхід, який тримається, лишає в шаблоні один повністю оформлений рядок деталей, клонує його по разу на запис, а тоді відкриває прогалину перед блоком підсумків, щоб усе, заякорене нижче, зсунулося вниз, не втративши свого форматування
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611'; // значення йде в якір, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1; // індекс у пулі від нуля, з боку клітинки від одиниці
// рядок 5 — оформлений шаблонний рядок деталей
for I := 0 to ItemCount - 1 do
Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1); // стилі й формули їдуть разом із ним
// відкрити прогалину над блоком підсумків; вміст нижче зсувається вниз
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);
Два рядки варті другого погляду. Присвоєння шрифту несе зсув на одиницю, який кусає мовчки: Fonts.Add повертає позицію в пулі від нуля, тоді як клітинка зберігає посилання на шрифт від одиниці, де 0 означає усталений шрифт, — тож пропуск + 1 нічого не підіймає, він просто оформлює ваш заголовок не тією гарнітурою. Другий рядок — CopyRange, що переносить форматування й формули разом зі значеннями. Це і є вся причина клонувати зроблений руками шаблонний рядок, замість відбудовувати його вигляд у коді. Дизайнер володіє виглядом один раз, у шаблоні; генератор тільки й робить, що наливає дані в його копії
Цей поділ масштабується далі, коли перевикористовне компонування живе у власній книзі — скажімо, аркуш зі смугами шапки й підвалу, спільними для кількох звітів. CopyRangeTo виконує той самий клон через межі аркушів, беручи цільовий аркуш плюс координати призначення, тож генератор може тримати один незайманий аркуш-шаблон і штампувати його області в стільки вихідних аркушів, скільки потребує завдання. Альтернатива — змінювати шаблон на місці й намагатися потім його відновити — з тих речей, що працюють до того дня, коли прогін урветься на півдорозі
Що InsertRows переносить, а чого ні
Підхід із вирощуванням шаблону працює лише тому, що InsertRows в XLSX — це структурна правка, а не перетасовування клітинок. Відкриваючи прогалину, він переміщує об’єднані області, висоти рядків, гіперпосилання, примітки, закріплені панелі, діапазони автофільтра, умовні формати, перевірки даних, таблиці, визначені імена, якорі зображень і якорі діаграм, що сидять нижче точки вставляння, а не самі лише значення клітинок. Саме це дає блокові підсумків прибути на новий рядок з неушкодженими об’єднаннями й числовими форматами, а не прибути обдертим
Два його задокументовані обмеження — саме те, навколо чого варто проєктувати. Коригування формул обмежене аркушем, який редагують: посилання всередині цього аркуша переписуються, і формула на іншому аркуші, що вказує в зсунуту область, теж переписується, але коригування йде лише за тими посиланнями, що цілять у редагований аркуш, тож будь-яка схема посилань між книгами заслуговує на власний аудит, а не на сліпу довіру. Друге обмеження гостріше, і воно на боці XLS. Зведені таблиці переживають цикли відкриття-збереження як сирі збережені записи, а не як змодельовані об’єкти, які HotXLS може посунути, тож вставляння рядків не переміщує слід зведеної таблиці. Будь-який шаблон, який ви будуєте для формату .xls, має паркувати свої зведені області добряче осторонь від будь-якої смуги, що росте
Відмова писати дані в простір компонування
Збій з об’єднаними клітинками, який справді доходить до продакшену, — не косметичний. Він структурний: рядок деталей сповзає в об’єднану смугу компонування, його значення приземляються в покриті клітинки й стають невидимими, а підсумки стовпців тихо перестають збігатися з тим, що бачить кожен, хто читає аркуш. Оскільки FindAt відповідає на питання про покривну область для будь-якої координати, генератор може відмовити в цьому записі тієї миті, коли він мав би статися, а не постачати звіт, що мовчки недораховує
// відмовитися писати дані деталей в об’єднану область компонування
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
raise Exception.CreateFmt('row %d overlaps a merged layout region', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;
Та сама перевірка меж доречна всюди, де користувач згодом сортуватиме чи фільтруватиме вивід. Діапазон з об’єднаннями всередині неможливо чисто відсортувати, бо сортування рухає рядки незалежно, а об’єднання, що охоплює кілька рядків, не має єдиного рядка, з яким могло б поїхати; Excel відповідає помилкою або переплутаним компонуванням. Дисципліна, що тримає звіти правильними, географічна. Обмежте об’єднання смугами заголовків, роздільниками секцій і блоками підписів, а табличну середину аркуша тримайте пласкою. Стаття про генерацію звітів за шаблонами розгортає цей поділ на компонування та дані в повноцінний робочий процес із заповнювачами, а стаття про умовне форматування й форматований текст розглядає оформлення цієї пласкої смуги даних
Як об’єднання деградують по дорозі назовні
Об’єднання — це поняття книги, і кожен текстово орієнтований формат експорту вшановує його різною мірою. Знання про ці три поведінки наперед заощаджує один цикл QA. Експорт у HTML відтворює об’єднання сумлінно, видаючи colspan і rowspan в одній таблиці, тож звіт для браузера зберігає свій смугастий вигляд. Експорт у RTF не розтягує стовпці взагалі: текст якоря приземляється у власній клітинці, а решта ширини об’єднання виходить порожніми клітинками, через що широкий заголовок у текстовому процесорі візуально збивається вліво. CSV поняття об’єднання не має взагалі, тож значення якоря займає одне поле, а кожна покрита клітинка виходить порожнім полем. Висновок для книги, що живить ще й експорти з роздільниками: тримайте все несуче поза об’єднаною геометрією; стаття про експорт у CSV, TSV і HTML докладно проходить кожен формат
Одне заспокоєння для тих, хто зважує це проти розміру файлу: об’єднання на масштабі звіту коштують майже нічого. Таблиця об’єднань крихітна поруч із даними клітинок, а читання покритої клітинки й далі йде через FindAt, а не через сканування. Тиск на продуктивність у великих книгах походить з іншого місця — головно з росту пулу стилів і пам’яті, яку тримає шлях збереження, — і саме це напряму розбирає стаття про продуктивність великих книг. Обидва API об’єднань, операції структурного редагування й демонстрації шаблонів постачаються з HotXLS Delphi Component