Technisch artikel

HotXLS Delphi Component: CSV, TSV, HTML, and RTF export in Delphi

Stel je een nachtelijke taak voor die in code een factuurwerkmap opbouwt en deze als CSV wegschrijft zodat een downstream systeem het kan importeren. De getallen zien er goed uit in Excel. De CSV opent netjes in een teksteditor. Dan verslikt de importer zich in de totaalkolom, omdat het bedragveld voor rij 42 =SUM(D2:D41) bevat, de formule als letterlijke tekst, niet het cijfer waar het naar zou moeten berekenen. Er is niets stuk. Dit is gedocumenteerd gedrag, en het is het eerste dat je moet begrijpen over exporteren vanuit HotXLS: de schrijver serialiseert het celmodel precies zoals het is, en een formulecel waarvan de waarde nooit is berekend, heeft alleen zijn formuletekst om te overhandigen

Waarom je CSV formules bevat in plaats van getallen

HotXLS slaat formuletekst en berekende waarde op als twee gescheiden zaken. SaveAsCSV voert onderweg naar buiten de rekenmachine niet uit, en dat is met opzet: een export mag de werkmap niet muteren, en mag niet het risico lopen vast te lopen op een pathologische formuleketen. Bestanden die Excel zelf heeft opgeslagen dragen gecachte resultaten naast de formules, dus het opnieuw exporteren daarvan gedraagt zich zoals je verwacht. De valkuil is specifiek voor werkmappen die je eigen code heeft gegenereerd, waar formules zijn geschreven maar nooit geëvalueerd. De oplossing is de waarden te laten bestaan vóórdat je exporteert, met dezelfde Calculate-engine die verwijzingen tussen bladen en aangepaste functies oplost:

Diagram dat toont dat een Delphi HotXLS-werkboekcel alleen formulettekst vasthoudt tot Book.Calculate de waarde berekent, zodat de CSV-export een getal uitzendt in plaats van =SUM-tekst
SaveAsCSV serialiseert het celmodel zoals het erbij ligt — zonder Calculate draagt het bedragveld letterlijke formuletekst en de importer weigert haar
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  R: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('invoice-run.xlsx');
    Sheet := Book.Sheets[0];

    // Materialiseer formuleresultaten zodat de CSV getallen bevat, geen '=...'-tekst
    for R := 2 to 41 do
      if Sheet.Cells[R, 4].Formula <> '' then
        Sheet.Cells[R, 4].Value := Book.Calculate(Sheet.Cells[R, 4].Formula);

    Book.SaveAsCSV('feed.csv', 0, ',');    // blad 0, komma
    Book.SaveAsCSV('feed.tsv', 0, #9);     // hetzelfde blad als TSV
  finally
    Book.Free;
  end;
end;

Let op wat de lus daadwerkelijk doet: hij overschrijft de formulecellen met hun berekende waarden. Dat is precies goed voor een wegwerpbare exportpas en verkeerd als je van plan bent de werkmap daarna weer als .xlsx op te slaan, want je hebt zojuist levende formules vervangen door bevroren getallen. Exporteer vanuit een kopie, of beperk de terugschrijving zodat die alleen de exportrun raakt. De engine achter Calculate gaat verder dan dit, inclusief het registreren van je eigen functies, wat het onderwerp is van de HotXLS-formule-engine en aangepaste functies

Wat de schrijver voor scheidingstekens garandeert

Het CSV-pad produceert UTF-8 met een byte-order mark, CRLF-regeleindes en RFC 4180-aanhalingstekens. Elk veld dat het scheidingsteken, een aanhalingsteken of een regeleinde bevat, wordt omwikkeld, en ingesloten aanhalingstekens worden verdubbeld. Datums worden weergegeven als yyyy-mm-dd hh:nn:ss, ongeacht de weergaveopmaak van de cel. Dat is de juiste keuze voor een machinale consument, hoewel het iedereen verrast die verwachtte dat de opmaak op het scherm zou worden overgenomen. Rich-tekstcellen worden platgeslagen door hun runs samen te voegen

Diagram van de ene HotXLS delimited-writer in Delphi die CSV produceert met een komma en TSV met #9, terwijl beide uitvoer UTF-8 BOM, CRLF-regeleinden en RFC 4180-quoting delen
CSV en TSV komen uit dezelfde writer, dus de UTF-8 BOM, CRLF-einden en RFC 4180-quoting gelden voor beide onveranderd

Die standaardwaarden regelen de meeste discussies met een importer voordat ze beginnen, maar twee ervan horen toch thuis in je interfacecontract. De eerste is de BOM. Die laat Excel het bestand openen met accenttekens intact, maar een handvol strikte parsers behandelen die drie bytes als data; als de jouwe daar één van is, strip ze dan bij de overdracht. De tweede is TSV. Dat is helemaal geen aparte functie, gewoon dezelfde schrijver aangeroepen met #9 als scheidingsteken, dus alles hierboven geldt er ongewijzigd voor. Het te exporteren blad wordt gekozen via een 0-gebaseerde index in de overload met meerdere argumenten, terwijl de verkorte vorm met één argument, SaveAsCSV(FileName), het actieve blad neemt

HTML-export is een momentopname, geen uitwisselingsformaat

Waar CSV alles weggooit behalve waarden, probeert SaveAsHTML het uiterlijk te behouden: één <table> per blad, samengevoegde gebieden uitgedrukt als colspan en rowspan, basale celopmaak inline als CSS. Thema-relatieve kleuren worden overgeslagen in plaats van opgelost, dus een sjabloon dat op themasleuven leunt komt er kaler uit dan het er in Excel uitziet. Stel expliciete RGB-kleuren in op alles wat de tocht moet overleven. Het opties-object regelt de envelop:

var
  Opts: TXLSXHtmlExportOptions;
begin
  Opts := TXLSXHtmlExportOptions.Create;
  try
    Opts.Title := 'Weekly settlement';
    Opts.TableClass := 'report-grid';     // aanknopingspunt voor het stylesheet van de hostpagina
    Opts.WriteDocument := True;           // volledige pagina, geen fragment
    if Book.SaveAsHTML('settlement.html', 0, Opts) <> 0 then
      raise Exception.Create('Sheet index out of range');
  finally
    Opts.Free;
  end;
end;

Twee details in dat fragment verdienen aandacht. Zet WriteDocument op False en de uitvoer wordt een kaal tabelfragment in plaats van een volledige pagina, wat je wilt wanneer je een voorbeeld injecteert in een bestaande lay-out: stel TableClass in en laat het hoststylesheet de opmaak doen. De returnconventie is ook het omgekeerde van de meeste HotXLS-aanroepen. SaveAsHTML geeft 0 terug bij succes en -1 voor een ongeldige bladindex, dus een uit gewoonte gecodeerde controle op = 1 zal elke geslaagde export als een mislukking rapporteren. Wanneer je een gebied nodig hebt in plaats van een heel blad, bijvoorbeeld om een enkel blok te e-mailen of in te sluiten, exporteert TXLSXRange.SaveAsHTML elk rechthoekig bereik onder dezelfde renderregels

RTF-uitvoer en waar het nog steeds zijn plaats verdient

Het vierde doelformaat schrijft RTF 1.6-tabellen, één blad per aanroep via SaveAsRTF. Kolombreedtes worden benaderd met ongeveer 96 twips per teken kolombreedte. De structurele beperking om te kennen is dat samengevoegde cellen in de uitvoer niet overspannen: alleen de ankercel draagt zijn inhoud, en de bedekte cellen komen als leeg naar buiten. Dat sluit RTF uit voor sjablonen met veel lay-out. Het verdient nog steeds zijn plaats als de weg van de minste weerstand om tabelresultaten in een tekstverwerker te laten vallen, of in een verouderd documentbeheersysteem van vóór HTML-inname

Round-trippen: CSV importeren is met opzet destructief

CSV weer inlezen heeft zijn eigen contract. OpenCSV wist de hele werkmap en bouwt hem opnieuw op als één blad genaamd Sheet1. Het is in de geest een constructor, geen samenvoeging, dus roep het nooit aan op een werkmap die nog onopgeslagen inhoud bevat. Het doorgeven van #0 als scheidingsteken activeert automatische detectie van het scheidingsteken. De vlag ADetectTypes regelt typepromotie: met deze aan worden numerieke strings getallen, ISO-8601-strings worden datums, en true/false worden booleans. Zet hem uit wanneer de feed identificatoren met voorloopnullen, postcodes of productcodes bevat, die allemaal door promotie stilzwijgend worden verminkt tot getallen (een voorloopnul is simpelweg verdwenen zodra 00123 123 wordt). Beide facades bieden dezelfde import. Combineer dit met de exportaanroepen hierboven en je hebt een formaatbrug die nergens in de pipeline een geïnstalleerde Excel nodig heeft, het scenario dat wordt behandeld in database-naar-Excel-rapportgeneratie met HotXLS

Rechtstreeks naar een stream exporteren

Elke schrijver hier heeft een stream-overload naast de bestandsnaamversie: CSV, HTML, RTF en de werkmapformaten zelf. In servercode zijn die overloads degene om naar te grijpen. Een webendpoint dat een CSV-download bedient, kan naar een TMemoryStream schrijven en die rechtstreeks aan het responsobject overhandigen, zonder tijdelijk bestand, zonder opruimtaak en zonder botsing tussen twee verzoeken die toevallig dezelfde gegenereerde naam kozen. Hetzelfde geldt voor het wegschrijven van exports naar blob storage of het bijvoegen ervan bij uitgaande mail. Het bestandssysteem valt volledig buiten beeld

Dat patroon versterkt zich met hoe de bibliotheek wordt ingezet. Beide facades zijn native Object Pascal-lezers en -schrijvers, dus er is geen Excel-installatie, geen COM-automatisering en geen per-proces-bottleneck die verzoeken op de server serialiseert. Elk verzoek kan zijn eigen workbook-object bezitten, de terugschrijving van berekeningen uit het eerste onderdeel uitvoeren en zijn export parallel met zijn buren streamen. Geheugen is de ene resource om in de gaten te houden. Het workbook-model leeft gedurende de export in RAM, dus een service die zeer grote bestanden opent alleen om ze opnieuw als CSV uit te zenden, moet gelijktijdige taken beperken, of de te grote in een wachtrij plaatsen, in plaats van een verkeerspiek de working set te laten bepalen

Nog een kleinere knop: zet IncludeBOM in de HTML-opties wanneer het fragment als zelfstandig bestand wordt opgeslagen dat een downstream tool controleert op codering. Wanneer je HTML rechtstreeks via HTTP serveert, laat de charset-declaratie dan over aan de responsheaders

Wanneer de bytes toch verkeerd uitkomen

De meest voorkomende supportvraag over CSV-export is het openingsprobleem in een ander jasje: Excel toont mojibake in plaats van accenttekens. Het instinct is de schrijver de schuld te geven, maar die zendt precies om deze reden een UTF-8-BOM uit, en het bestand is bijna altijd correct wanneer het je code verlaat. Iets tussen daar en Excel heeft de BOM opgegeten. Een FTP-overdracht in tekstmodus, een streamkopie die de eerste drie bytes overslaat, een proxy die onderweg opnieuw codeert: elk daarvan zal de markering strippen en Excel laten gokken naar de codering, wat het slecht doet. Diagnosticeer dit op de grens, niet in de exportaanroep. Open het afgeleverde bestand in een hexviewer en bevestig dat EF BB BF nog steeds het eerste is wat erin staat

Diagram dat nagaat hoe een correcte UTF-8 BOM geschreven door HotXLS CSV-export in Delphi wordt gestript door een FTP-overdracht in tekstmodus of een hercoderende proxy, waarna Excel mojibake toont
De writer zendt EF BB BF correct uit — mojibake verschijnt pas nadat een transport de marker heeft afgeplukt, dus diagnoseer de geleverde bytes in een hex-viewer

Dat is de rode draad voor alle vier formaten. De exportaanroep is het makkelijke deel, en HotXLS maakt bij elke beslissing waar de schrijver voor staat een verdedigbare keuze. De mislukkingen zitten in de naden, waar formuletekst een parser tegenkomt die een getal wilde, waar een BOM een transport tegenkomt dat het niet bewaart, waar een samengevoegde cel het platte tabelmodel van RTF tegenkomt. Elk daarvan is een feit om vast te leggen in het contract tussen jouw exporteur en wat het ook consumeert, want de consument kan je bedoelingen niet uit de bytes lezen. Voor de volledige methodelijst over beide workbook-facades bevat de productpagina van HotXLS Delphi Component de volledige referentie