Technisch artikel

HotXLS Delphi Component: template-based report generation in Delphi

De betrouwbare manier om een opgemaakt Excel-rapport te produceren vanuit Delphi is te beginnen bij een werkmap die een ontwerper al heeft gebouwd. Iemand van finance maakt de lay-out van de factuur in Excel: het logo, de kolomkoppen, de randen op de detailband, de vetgedrukte totaalrij, de valutanotaties. Je code opent dat bestand, laat live data in de cellen vallen die de ontwerper ervoor heeft gereserveerd, en slaat het resultaat op. Het uiterlijk is van hen; de getallen zijn van jou. HotXLS, een native bibliotheek voor Delphi en C++Builder die XLS- en XLSX-werkmappen leest en schrijft zonder Excel aan te sturen, geeft je de drie bewerkingen die deze aanpak nodig heeft: zoeken naar een cel op basis van tekst, een bereik kopiëren met zijn stijlen en formules intact, en rijen invoegen zodat alles eronder mee naar beneden schuift met de data

De ene regel die een generator die sjabloonwijzigingen overleeft onderscheidt van een die bij de eerste breekt, is cellen nooit aan te spreken via letterlijke rij- en kolomnummers. Een sjabloon is een document dat andere mensen bewerken. Het financeteam voegt een belastingregel toe, verhoogt de hoogte van de logorij, herordent het adresblok, en het bestandsformaat helpt je totaal niet: een BIFF- of OOXML-opslag slaagt ongeacht of rij 10 nog betekent wat het vorig kwartaal betekende. Een generator die de eerste detailregel naar een hardgecodeerde rij 10 schrijft, zal, de eerste keer dat iemand een blok boven de detailsectie invoegt, regelitems over de verkeerde cellen stempelen en een totaalbereik optellen dat de data niet meer dekt. Er wordt niets geworpen, elke opslag geeft succes terug, en het enige signaal is een klant die een verkeerde factuur opmerkt

Diagram van de HotXLS-sjabloonpijplijn in Delphi: tokens verankeren met FindText, de detailband uitbreiden, het berekende totaal verifiëren, dan opslaan
Templaterapportgeneratie in Delphi verloopt als vier HotXLS-fasen: veranker de tokens, breid de detailband uit, verifieer het berekende totaal en lever dan af

Verankeer elke coördinaat aan een placeholder-token

De oplossing is het sjabloon zijn eigen coördinaten te laten dragen. De ontwerper schrijft tokens zoals {{CUSTOMER}}, {{DATE}} en {{DETAIL_START}} in de cellen die de generator moet aanraken, en de generator bepaalt elke positie tijdens runtime aan de hand van waar het die tokens vindt. Lay-outwijzigingen maken niet meer uit, omdat het token meebeweegt met de cel waarin het zit. De tweede helft van het contract is de faalregel: als een verplicht token ontbreekt, stopt de taak voordat er klantdata in het bestand terechtkomt. Een sjabloon dat is afgedreven moet een mislukt jobticket opleveren, geen afgeleverd document

De tokens vinden: FindText en ReplaceText

Beide HotXLS-klassenfamilies bieden zoeken op werkbladniveau. FindText geeft de rij en kolom van de eerste cel terug waarvan de tekst overeenkomt, met een overload die hoofdlettergevoeligheid toevoegt. ReplaceText wisselt elke voorkomst en geeft terug hoeveel het er heeft veranderd. De twee dekken de twee soorten tokens die je meestal hebt. Een enkel anker zoals de klantnaam, dat je één keer lokaliseert en waarnaast je schrijft; een token dat precies één keer zou moeten voorkomen, zoals de rapportdatum, dat je vervangt en waarvan je het aantal controleert. Aan de XLSX-kant ziet een vulling die zichzelf op deze manier verankert er zo uit:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R, C: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('invoice-template.xlsx') <> 1 then
      raise Exception.Create('Cannot open invoice template');
    Sheet := Book.Sheets[0];               // TXLSXSheets.Items is 0-gebaseerd

    if not Sheet.FindText('{{CUSTOMER}}', R, C) then
      raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
    Sheet.Cells[R, C].Value := 'ACME Corp';

    if Sheet.ReplaceText('{{DATE}}',
         FormatDateTime('yyyy-mm-dd', Date)) = 0 then
      raise Exception.Create('Template drift: {{DATE}} token missing');
    // detailexpansie en opslaan volgen hieronder
  finally
    Book.Free;
  end;
end;

Twee details zijn van belang. Ten eerste komen FindText en ReplaceText overeen met de tekstwaarde van een cel; een token ingebed in een formulestring is voor hen onzichtbaar, dus placeholder-tokens horen thuis in gewone cellen, nooit binnen formules. Ten tweede is het aantal vervangingen je afwijkingsdetector. Een sjabloon dat precies één {{DATE}}-token zou moeten bevatten maar nul vervangingen meldt, is bewerkt, en op dat moment een uitzondering opwerpen is precies wat stille lay-outafwijking omzet in een zichtbare mislukking

De detailrij klonen zonder stijlen of formules te verliezen

De detailsectie van een factuur groeit mee met de data. Waarden rechtstreeks in lege rijen onder de voorbeeldregel schrijven, gooit alles weg wat de ontwerper heeft voorbereid: de randen, de getalnotaties, de formules per rij. Het patroon dat dat allemaal behoudt, is één volledig opgemaakte voorbeeldrij in het sjabloon te laten staan en die voor elk item te klonen. CopyRange dupliceert stijlen en formules in één aanroep, waarna de generator alleen de waardecellen overschrijft

Diagram van tokenankers in een HotXLS Delphi-sjabloon waarin een ontbrekende placeholder de taak laat falen voordat er data wordt geschreven
Templatetokens dragen hun eigen coördinaten, en een ontbrekend token stopt de taak voordat er ook maar enige data wordt geschreven
const
  DetailRow = 10;            // de opgemaakte voorbeeldrij in het sjabloon
var
  I: Integer;
begin
  // Open eerst ruimte vóór het totalenblok, zodat het SUM-bereik
  // onder de detailband meegroeit met de data.
  if Length(Items) > 1 then
    Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);

  for I := 0 to High(Items) do
  begin
    if I > 0 then              // kloon stijlen + formules van de voorbeeldrij
      Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
    Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
    Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
    Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
    Sheet.Cells[DetailRow + I, 4].Formula :=
      Format('B%d*C%d', [DetailRow + I, DetailRow + I]);  // geen '='-prefix
  end;
end;

Let goed op de formuletoewijzing. De XLSX-eigenschap Formula neemt de expressie zonder voorafgaand isgelijkteken, terwijl de XLS-facade '=B10*C10' verwacht, toegewezen via Value. Deze twee conventies mengen is de meest voorkomende overzetfout tussen de klassenfamilies, en het faalt zonder klacht: de cel bevat gewoon een letterlijke string die Excel als tekst toont. Als het sjabloon de detailband versiert met samengevoegde titelrijen, onthoud dan dat alleen de linkerboven-cel van een samengevoegd gebied een waarde draagt. De lay-outregels in het begeleidende artikel over samengevoegde cellen in lay-outgestuurde rapportsjablonen leggen uit waarom samenvoegingsregio's volledig buiten de databand horen

Wat InsertRows verplaatst, en wat het achterlaat

Rijen invoegen vóór het totalenblok is wat een SUM-bereik laat meegroeien terwijl de detailsectie groeit. Aan de XLSX-kant draagt InsertRows een lange lijst afhankelijke structuren mee met de cellen: samengevoegde bereiken, rijhoogtes, hyperlinks, opmerkingen, bevroren deelvensters, autofilterbereiken, voorwaardelijke opmaak, gegevensvalidaties, tabellen, gedefinieerde namen, en afbeeldings- en grafiekankers. Er is één grens in die lijst die het onthouden waard is. Formuleherschrijving reikt alleen tot verwijzingen binnen hetzelfde blad. Een formule op een samenvattingsblad die naar het verplaatste gebied wijst, houdt zijn oude coördinaten en leest stilletjes de verkeerde cellen, en dat is waarom totalen die over bladen worden opgehaald veiliger worden uitgedrukt via werkmapniveau-namen. Het begeleidende artikel over gedefinieerde namen en cross-sheet-formules werkt dat patroon verder uit

Het legacy XLS-formaat trekt de grens op een lastigere plek. HotXLS bewaart draaitabellen, querytabellen en externe gegevensverbindingen in BIFF-bestanden als ruwe bytenblokken. Ze overleven openen en opslaan ongewijzigd, maar ze zijn niet gemodelleerd, dus rijinvoeging raakt ze nooit aan. Een sjabloon dat een draaitabel onder een uitdijend detailblok parkeert, slaat zonder enige waarschuwing op terwijl de bronrechthoek van de draaitabel van de data wegdrijft. De uitweg is structureel, niet defensief: houd draaitabel- en query-inhoud op bladen waar de generator nooit in invoegt, en het verouderen kan niet gebeuren

Diagram van wat HotXLS InsertRows verplaatst in XLSX en de grenzen van cross-sheet-formules en BIFF-pivots die Delphi-generatoren moeten respecteren
InsertRows draagt afhankelijke structuren door op XLSX, terwijl cross-sheet-formules en BIFF raw-blokken de grenzen markeren

Herberekenen vóór aflevering, of weten waarom je het hebt overgeslagen

HotXLS evalueert formules niet tijdens SaveAs. Wanneer een mens het bestand opent, herberekent Excel alles (de XLS-facade biedt CalculationMode en RecalcOnSave als je dat moet sturen), dus een rapport dat naar een menselijke inbox gaat, heeft verder niets van je nodig. Het beeld verandert op het moment dat de werkmap een ander programma voedt. CSV-export schrijft formules als hun letterlijke tekst en berekent ze nooit, en elke downstream parser die gecachte waarden vertrouwt, zal verouderde getallen of lege velden lezen. Voor die paden bereken je op de server met Calculate, die een willekeurige expressie evalueert tegen de geladen werkmap en het resultaat teruggeeft:

var
  Total: Variant;
  LastDetail: Integer;
begin
  LastDetail := DetailRow + Length(Items) - 1;
  Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
    [DetailRow, LastDetail]));
  if (not VarIsNumeric(Total)) or
     (Abs(Total - ExpectedTotal) > 0.005) then
    raise Exception.Create('Invoice total does not match the order record');

  if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
    raise Exception.Create('Save failed: check output path and permissions');
end;

Het berekende totaal controleren tegen het bestelrecord vóór het opslaan is goedkope verzekering met een goede opbrengst. Het verandert een verkeerde factuur in een mislukte taak. Een operator kan een mislukte taak binnen seconden opnieuw proberen; een verkeerde factuur die al in de mailbox van een klant zit, kost een accountmanager een excuus en een correctie

Twee klassenfamilies, één algoritme

Dezelfde logica draagt over tussen formaten, maar niet dezelfde code. TXLSWorkbook voor legacy .xls is interface-gebaseerd en referentiegeteld, met 1-gebaseerde bladindexering, en je geeft het nooit met de hand vrij. TXLSXWorkbook voor .xlsx is een gewoon object dat je moet vrijgeven in een try..finally, met 0-gebaseerde bladindexering en de hierboven getoonde formuleconventie. FindText, ReplaceText, CopyRange en InsertRows bestaan aan beide kanten, dus de anker-kloon-herbereken-vorm draagt netjes over. Het praktische advies is je te committeren aan één formaat per pipeline, of de twee objectlevenscycli te verbergen achter een eigen dunne adapter in plaats van het verschil door de generator te verspreiden

Omvang doet er zelden toe voor het soort rapport dat dit patroon produceert. Een opgemaakte rij een paar duizend keer klonen is niets voor huidige hardware. Het opslagpad wordt pas het knelpunt wanneer een detailband zescijferige rijaantallen bereikt, en op dat moment stuurt het instellen van StreamingWrite werkblad-XML rechtstreeks naar het uitvoerpakket in plaats van het te bufferen; het artikel over streaming writes voor server-batchtaken behandelt wanneer die ruil de moeite waard is. Grafieken gedragen zich zoals de rest van de lay-out: aan de XLSX-kant bewegen zowel het grafiekanker als zijn reeksverwijzingen mee wanneer InsertRows erboven draait, zodat een grafiek onder de totaalrij aan de juiste data gebonden blijft, terwijl aan de XLS-kant grafieken op hun eigen grafiekbladen zitten en, net als draaitabellen, nooit verschuiven. Dat is nog een argument om presentatiebladen vrij te houden van het blad dat de generator uitbreidt

Deze anker-kloon-herbereken-aanpak laat een ontwerper eigenaar zijn van hoe een werkmap eruitziet, terwijl je code eigenaar is van wat er staat, wat meestal is wat gegenereerde Excel-uitvoer de moeite van het onderhouden waard maakt. De zoek-, kopieer- en invoegaanroepen die hier zijn getoond, samen met de formule-engine die wordt gebruikt voor de totaalcontrole vóór aflevering, worden geleverd met de HotXLS Delphi Component voor Delphi en C++Builder