Artykuł techniczny

HotXLS: komórki scalone i układ szablonów raportów

Przejdź po komórkach świeżo otwartego szablonu raportu, a scalony tytuł zachowa się jak przeciek. Czytasz A1 i dostajesz "Quarterly Statement"; czytasz B1 do F1, które widocznie siedzą pod tym samym transparentem, i nie dostajesz nic. Wpisz wartość do C1, żeby załatać nagłówek, a ona nigdy się nie pojawi. Siatka nie zgubiła twoich danych. Robi dokładnie to, co znaczy scalenie: zarówno w XLS, jak i w XLSX scalony prostokąt renderuje treść jednej komórki, tej kotwiczącej w lewym górnym rogu, a resztę traktuje jak przykrytą przestrzeń, która trzyma wartości, ale nigdy ich nie pokazuje. Użytkownicy Excela przyswajają to metodą prób i błędów. Generator raportów musi zakodować to jako regułę, bo w generowanym kodzie objawem jest pusty obszar bez żadnego wyjątku, do którego dałoby się go prześledzić. HotXLS, natywna biblioteka Object Pascal czytająca i zapisująca oba formaty Excela z Delphi i C++Buildera, wystawia tablicę scaleń na tyle jawnie, że możesz programować wobec tej reguły, zamiast odkrywać ją ponownie w zgłoszeniu do wsparcia

Jedna wartość, jedna kotwica

Scalenie to instrukcja wyświetlania nałożona na siatkę, która nie zmienia kształtu. Każda przykryta komórka nadal istnieje w pliku jako własne miejsce; rekord scalenia mówi tylko konsumentowi, by rozmalował treść kotwicy na cały prostokąt. To rozróżnienie napędza trzy zachowania warte przyswojenia, zanim napiszesz jakikolwiek kod układu. Odczyt przykrytej komórki zwraca jej własną przechowywaną wartość, która dla transparentu zbudowanego przez ciebie jest zwykle pusta, więc każdy kod oglądający scalony tytuł musi rozwiązać i przeczytać kotwicę. Zapis do przykrytej komórki udaje się na poziomie pliku i nie pokazuje się nigdzie, czyli mamy pułapkę niewidzialnego nagłówka z otwarcia. A rozscalenie obszaru odsłania to, co siedziało pod nim przez cały czas, więc zabłąkana wartość wpisana w przykrytą przestrzeń zamienia się w widoczną usterkę w dniu, w którym ktoś rozpuści scalenie

Diagram scalonego transparentu HotXLS, w którym przykryte komórki zachowują własne miejsca, podczas gdy odczyty rozwiązują się do kotwicy A1 w arkuszach Delphi
HotXLS zachowuje każdą przykrytą komórkę jako prawdziwe miejsce i przemalowuje tylko kotwicę, więc odczyty rozwiązują się przez A1, a zapisy w przykrytą przestrzeń zostają niewidoczne aż do rozscalenia

Po stronie XLSX ta tablica jest obiektem pierwszej klasy. Sheet.MergedCells niesie Add('A1:C1'), FindAt(Row, Col), DeleteAt i Items, a wywołaniem, po które sięgasz najczęściej, jest FindAt: podaj mu dowolną współrzędną, a zwróci scalony obszar przykrywający tę komórkę albo nil, gdy komórka stoi sama. To jedno wyszukanie jest fundamentem obu połówek poprawnej obsługi scaleń — bezpiecznego odczytu i strażnika zapisu — a obie pojawią się dalej

Dwie fasady, dwa idiomy scalania

HotXLS trzyma klasyczny silnik BIFF8 dla .xls i silnik OOXML dla .xlsx jako osobne modele obiektowe, a scalanie zapisują one różnie, bo wywodzą się z różnych konwencji. Fasada XLS idzie za idiomem COM Excela: bierzesz zakres z dwuargumentowej właściwości indeksowanej i wołasz Merge z OleVariant, którego wartość decyduje o geometrii, jaką dostaniesz

var
  Book: IXLSWorkbook;   // zliczany interfejsowo: bez ręcznego Free
  Sh: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  Sh := Book.Sheets[1];                 // kolekcja arkuszy XLS liczy od jedynki
  Sh.Range['A1', 'F1'].Merge(False);    // False = jeden scalony blok
  Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
  Sh.Range['A3', 'F4'].Merge(True);     // True = scal wszerz: jedno scalenie na wiersz
  Book.SaveAs('layout.xls');
end;

Argument Merge jest tym, co ludzie mylą. Na zakresie dwuwierszowym Merge(True) produkuje dwa niezależne jednowierszowe scalenia, czyli excelowe "Scal wszerz" i dokładnie to, czego chcesz przy piętrowym pasie nagłówka, którego wiersze mają zostać rozdzielne. Merge(False) zlewa cały prostokąt w jeden blok. Zakres raportuje też MergeCells jako flagę stanu, zwraca obejmujący obszar przez MergeArea i rozpuszcza się przez Unmerge. Fasada XLSX wystawia te same operacje pod innymi nazwami: Sheet.MergeCells(Row1, Col1, Row2, Col2) bierze granice całkowite, TXLSXRange.Merge przyjmuje równoważny wariant Across, a kolekcja MergedCells trzyma wynik

Szablon, który rośnie razem ze swoimi danymi

Prawdziwy szablon raportu nie jest stałą siatką. Nagłówek i sumy są stałe, ale sekcja szczegółów między nimi rozciąga się do tego, co zwróci zapytanie. Wzorzec, który się broni, trzyma w szablonie jeden w pełni ostylowany wiersz szczegółu, klonuje go raz na rekord, a potem otwiera przerwę przed blokiem sum, żeby wszystko zakotwiczone poniżej zjechało w dół, nie tracąc formatowania

Szablon raportu HotXLS rosnący w Delphi: ostylowany wiersz szczegółu jest klonowany na każdy rekord, a InsertRows otwiera przerwę, żeby blok sum zjechał w dół z nietkniętymi scaleniami
Klonowanie ostylowanego wiersza szczegółu przenosi jego style i formuły do każdej kopii, a InsertRows zsuwa potem pas sum w dół z nietkniętymi scaleniami i formatami
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611';    // wartość idzie do kotwicy, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1;        // indeks puli od zera, strona komórki od jedynki

// wiersz 5 to ostylowana linia szablonu szczegółu
for I := 0 to ItemCount - 1 do
  Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1);             // style i formuły jadą razem z nim

// otwórz przerwę nad blokiem sum; treść poniżej zjeżdża w dół
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);

Dwie linie zasługują na drugie spojrzenie. Przypisanie czcionki niesie błąd o jeden, który gryzie po cichu: Fonts.Add oddaje pozycję w puli liczoną od zera, podczas gdy komórka przechowuje referencję czcionki liczoną od jedynki, gdzie 0 znaczy czcionkę domyślną, więc pominięcie + 1 niczego nie podnosi, tylko styluje twój tytuł niewłaściwym krojem. Drugą linią jest CopyRange, które przenosi formatowanie i formuły razem z wartościami. To cały powód, by klonować ręcznie zbudowany wiersz szablonu, zamiast odtwarzać jego wygląd w kodzie. Projektant jest właścicielem wyglądu raz, w szablonie; generator tylko wlewa dane do jego kopii

Ten podział skaluje się dalej, gdy układ wielokrotnego użytku mieszka we własnym skoroszycie, powiedzmy jako arkusz pasów nagłówka i stopki współdzielonych przez raporty. CopyRangeTo wykonuje ten sam klon przez granice arkuszy, biorąc arkusz docelowy plus współrzędne miejsca docelowego, więc generator może trzymać jeden nieskazitelny arkusz szablonu i odbijać jego obszary w tylu arkuszach wyjściowych, ile zadanie potrzebuje. Alternatywa, czyli mutowanie szablonu w miejscu i próba przywrócenia go potem, jest z gatunku rzeczy, które działają aż do dnia, w którym przebieg przerwie się w połowie

Co InsertRows przenosi, a czego nie

Wzorzec rosnącego szablonu działa tylko dlatego, że InsertRows w XLSX jest edycją strukturalną, a nie przetasowaniem komórek. Kiedy otwiera przerwę, przenosi scalone obszary, wysokości wierszy, hiperłącza, komentarze, zamrożone panele, zakresy autofiltru, formaty warunkowe, walidacje danych, tabele, nazwy zdefiniowane, kotwice obrazów i kotwice wykresów, które siedzą poniżej punktu wstawienia, nie tylko wartości komórek. To właśnie pozwala blokowi sum przybyć do nowego wiersza z nietkniętymi scaleniami i formatami liczbowymi zamiast przybyć obdartym

Jego dwa udokumentowane ograniczenia to te, wokół których warto projektować. Dostosowanie formuł ma zasięg edytowanego arkusza: referencje wewnątrz tego arkusza są przepisywane, a formuła na innym arkuszu wskazująca w przesunięty obszar też jest przepisywana, ale dostosowanie idzie tylko za referencjami celującymi w edytowany arkusz, więc każdy schemat referencji międzyskoroszytowych zasługuje na własny audyt, a nie na ślepe zaufanie. Drugie ograniczenie jest ostrzejsze i leży po stronie XLS. Tabele przestawne przeżywają cykle otwarcia i zapisu jako surowe zachowane rekordy, a nie jako modelowane obiekty, które HotXLS może przesunąć, więc wstawianie wierszy nie przenosi śladu tabeli przestawnej. Każdy szablon, który budujesz dla formatu .xls, powinien parkować swoje obszary przestawne z dala od jakiegokolwiek rosnącego pasa

Odmowa zapisu danych w przestrzeń układu

Awaria scalonych komórek, która naprawdę dociera na produkcję, nie jest tą kosmetyczną. Jest strukturalna: wiersz szczegółu dryfuje w scalony pas układu, jego wartości lądują w przykrytych komórkach i stają się niewidoczne, a sumy kolumn po cichu przestają zgadzać się z tym, co widzi każdy czytający arkusz. Ponieważ FindAt odpowiada na pytanie o obszar przykrywający dla dowolnej współrzędnej, generator może odmówić takiego zapisu w chwili, w której by nastąpił, zamiast wysyłać raport, który po cichu zaniża wyniki

// odmów zapisu danych szczegółowych w scalony obszar układu
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;

Ta sama kontrola granic należy się wszędzie tam, gdzie użytkownik będzie później sortował albo filtrował wyjście. Zakres ze scaleniami w środku nie da się czysto posortować, bo sortowanie przesuwa wiersze niezależnie, a scalenie rozpięte na wierszach nie ma jednego wiersza, z którym mogłoby podróżować; Excel odpowiada błędem albo pogmatwanym układem. Dyscyplina, która utrzymuje raporty poprawnymi, jest geograficzna. Zamknij scalenia w pasach tytułowych, przekładkach sekcji i blokach podpisu, a tabelaryczny środek arkusza trzymaj płaski. Artykuł o generowaniu raportów z szablonów rozwija ten podział na układ i dane w pełny przepływ oparty na symbolach zastępczych, a artykuł o formatowaniu warunkowym i tekście formatowanym omawia stylowanie tego płaskiego pasa danych

Jak scalenia degradują się w drodze na zewnątrz

Scalenie to pojęcie skoroszytowe, a każdy format eksportu zorientowany na tekst honoruje je w innym stopniu. Znajomość tych trzech zachowań z góry oszczędza jeden cykl QA. Eksport HTML odtwarza scalenia wiernie, emitując colspan i rowspan w jednej tabeli, więc raport przeznaczony do przeglądarki zachowuje swój pasiasty wygląd. Eksport RTF w ogóle nie rozciąga kolumn: tekst kotwicy ląduje we własnej komórce, a pozostała szerokość scalenia wychodzi jako puste komórki, przez co szeroki tytuł wygląda w edytorze tekstu na wepchnięty do lewej. CSV nie ma pojęcia scalenia, więc wartość kotwicy zajmuje jedno pole, a każda przykryta komórka wychodzi jako pole puste. Wniosek dla skoroszytu, który zasila też eksporty z separatorami, brzmi: trzymaj wszystko nośne poza scaloną geometrią; artykuł o eksporcie CSV, TSV i HTML przechodzi przez każdy format szczegółowo

Scalony tytuł HotXLS wyeksportowany z Delphi do HTML z colspan i rowspan, do RTF bez rozciągnięć i do CSV jako spłaszczone pola
Ten sam scalony tytuł przeżywa eksport HTML dzięki colspan i rowspan, degraduje się w RTF do samotnej komórki przyklejonej do lewej i spłaszcza się w CSV do wartości plus pustych pól

Jedno zapewnienie dla każdego, kto waży to wobec rozmiaru pliku: scalenia kosztują w skali raportu prawie nic. Tablica scaleń jest maleńka przy danych komórek, a odczyt przykrytej komórki nadal idzie przez FindAt, a nie przez skanowanie. Presja wydajnościowa na dużych skoroszytach bierze się skądinąd, głównie z rozrostu puli stylów i pamięci trzymanej przez ścieżkę zapisu, czym wprost zajmuje się artykuł o wydajności dużych skoroszytów. Oba API scaleń, operacje edycji strukturalnej i dema szablonów przychodzą z pakietem HotXLS Delphi Component