Teknisk artikel

HotXLS Delphi Component: Office-free workbook automation in Delphi

Om en servers enda uppgift är att skriva ut Excel-filer har den ingenting att göra med att köra Excel. Att installera Office på en byggagent eller en rapporttjänst för att styra den via COM-automation är fel design, och det har varit fel design lika länge som praxisen har existerat. Microsoft säger det själva, i vägledning som inte har mjuknat på tjugo år: Office är varken byggt eller licensierat för att automatiseras från en obevakad, serversidig process. Det rätta svaret är att skriva BIFF- och OOXML-byten direkt, utan Excel i bilden alls. Det är hela premissen för HotXLS, ett nativt Object Pascal-bibliotek som läser och skriver kalkylbladsformaten själv, så det finns ingen skrivbordsapplikation att hänga sig, läcka, eller betala per licens för

Varför att styra EXCEL.EXE från en tjänst misslyckas

COM-automation fjärrstyr ett skrivbordsprogram, och ett skrivbordsprogram antar tyst tre saker en Windows-tjänst inte kan ge det: en laddad användarprofil, en interaktiv fönsterstation, och en människa som tittar på skärmen. Ta bort de förutsättningarna och felen kommer i en form ingen utvecklarmaskin någonsin återskapar. En återställningsprompt för filer, ett tilläggsfel, eller en licensaktiveringsdialog öppnas på ett skrivbord ingen kan se, och automationsanropet som utlöste det returnerar aldrig. Anroparen får så småningom timeout och dör; Excel-instansen gör det ofta inte, och överlever som en föräldralös process som håller fillås och förgiftar nästa körning. Alla som har sett elva lösa EXCEL.EXE-processer hopa sig under ett tjänstekonto känner till resten av den historien

Diagram som kontrasterar en Delphi-tjänst som styr EXCEL.EXE genom COM-automation, där dolda dialoger och föräldralösa processer blockerar anrop, med HotXLS som skriver BIFF8- och OOXML-arbetsboksbyte direkt i processen
COM-automation ärver ett skrivbordsprograms saknade förutsättningar, medan HotXLS skriver BIFF8- och OOXML-byte direkt utan något att installera på servern

Skalningshistorien är inte bättre ens när inget kraschar. En Excel-instans är en pipeline för en enda arbetsbok, varje egenskapsåtkomst betalar kostnaden för COM-marshalning mellan processer, och boxen som kör koden bär en Office-licens vars villkor utesluter precis det här användningsfallet. De flesta team möter de här gränserna en driftstörning i taget, vilket ungefär är hur "pensionera COM-lagret" hamnar på en färdplan

Innan den omskrivningen börjar, avgör en omfångsfråga, eftersom den avgör hur mycket av arbetet som är verkligt. COM-kod sätter nästan aldrig bara cellvärden. Den anropar Workbook.SaveAs med formatkonstanter, tvingar fram omberäkning, driver utskriftsinställningar, når ibland efter urklipp. Gå igenom den gamla koden och skriv ner vilka av de beteendena som faktiskt levereras i utdatan, eftersom var och en hamnar i ett annat hörn av ett nativt bibliotek, och ett par av dem (urklippsinteraktion är det uppenbara exemplet) har ingen serversidig betydelse och bör släppas i stället för porteras

Två nativa motorer, två ägandemodeller

HotXLS byter ut Excel-processen mot två direkta formatimplementationer. En BIFF8-postströmsmotor (TXLSWorkbook, enheten lxHandle) hanterar .xls. En OOXML-paketskrivare (TXLSXWorkbook, enheten lxHandleX) producerar .xlsx som följer ECMA-376 / ISO/IEC 29500. Det finns inget att registrera och inget att installera på servern, och du kan hålla så många arbetsböcker öppna samtidigt som minnet tillåter

Det som får folk att snubbla tidigt är att de två fasaderna äger sitt minne olika, och skillnaden är tyst tills den kraschar:

var
  Book: IXLSWorkbook;          // gränssnittsreferens: frigörs automatiskt
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // vanligt objekt: du frigör det
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls-utdata - inget Free; gränssnittets referensräkning äger det
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // OOXML .xlsx-utdata - explicit livstid
  BookX := TXLSXWorkbook.Create;
  try
    SheetX := BookX.Sheets.Add('Report');
    SheetX.Cells[1, 1].Value := 'Generated without Excel';
    BookX.SaveAs('report.xlsx');
  finally
    BookX.Free;
  end;
end;

XLS-fasaden är referensräknad via gränssnittet IXLSWorkbook. Deklarera variabeln som gränssnittstypen och anropa aldrig Free på den; håll samma objekt i en vanlig objektvariabel och frigör det själv, och referensräkningen frigör det en andra gång. XLSX-fasaden är ett vanligt objekt som vill ha en vanlig try..finally. Celladressering är 1-baserad på båda sidor, vilket är den enda platsen de två är överens. Bladsamlingarna är det inte: Entries på XLS-sidan är 1-baserad, XLSX-indexeraren Items är 0-baserad, och det avvikelsen-med-ett kompileras felfritt oavsett åt vilket håll du gör fel och visar sig bara vid körning

Att skriva en arbetsbok direkt in i ett HTTP-svar

En serversidig export har vanligtvis ingen anledning att röra disken. Temporära filer kräver en städpolicy, kolliderar under samtidiga förfrågningar, och lämnar kunddata liggande på volymer ingen tänkte på att granska. Båda fasaderna tar en TStream genom sina SaveAs-överlagringar, så arbetsboken kan gå direkt in i svaret:

Diagram som jämför de två HotXLS Delphi-fasaderna: TXLSWorkbook som frisläpps automatiskt genom IXLSWorkbook-gränssnittsreferensräkning, och TXLSXWorkbook som ett vanligt objekt som behöver en explicit Free i ett try..finally-block
XLS-fasaden släpps av referensräkning på interface medan XLSX-fasaden behöver ett uttryckligt Free, och bladsamlingarna skiljer sig mellan ett-baserade Entries och nollbaserade Items
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // skriver från STRÖMMENS aktuella position
  Mem.Position := 0;         // spola tillbaka innan strömmen lämnas över
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // ramverket äger nu Mem
finally
  Book.Free;
end;

Tillbakaspolningen är raden som förtjänar sin kommentar. SaveAs(Stream) skriver från strömmens aktuella position och söker aldrig tillbaka till noll efteråt. Glöm Mem.Position := 0 och klienten får en nedladdning på noll byte, eller så säger Excel att filen är korrupt. Det här är den vanligaste buggen i webbriktad arbetsbokskod, och den grymmaste, eftersom den seglar förbi vilket enhetstest som helst som bara kontrollerar att strömmen har en längd skild från noll

En enda arbetsboksbyggande rutin når varje annat leveransformat utan omstrukturering. SaveAsCSV svarar på "ge mig bara rådatan"-begäran, SaveAsHTML hanterar "släpp in det på en portalsida," SaveAsRTF matar dokumentpipelines, och SaveAsODS täcker ett OpenDocument-mandat, alla med både fil- och strömöverlagringar. En enda exportrutin plus en formatparameter ersätter vad som brukade vara fyra separata COM-makron. HTML-exportörens TXLSXHtmlExportOptions bär titel, CSS-klass och en fragment-eller-hel-dokument-omkopplare, vilket håller portalfallet borta från att behöva regex-redigera exporterad markup

Diagram över en Delphi-förfrågningshandläggare som sparar en HotXLS-arbetsbok i en TMemoryStream, spolar tillbaka Mem.Position till noll, och räcker strömmen till HTTP-svaret, med exportörerna för CSV, HTML, RTF och ODS jämte
Att spara till en TMemoryStream och spola tillbaka den före överlämningen skickar arbetsboksbyte rakt till klienten, och en exportrutin täcker skrivarna för CSV, HTML, RTF och ODS

Formelvärden utan en Excel-process som beräknar dem

Under COM-automation beräknade Excel om allt gratis, och att släppa COM återkallar det tyst. SaveAs lagrar formler som text utan att utvärdera dem; talen dyker bara upp när Excel öppnar filen och beräknar om, beteende XLS-fasaden låter dig finjustera via RecalcOnSave och CalculationMode. För en fil på väg till en människa är det precis rätt. Det är fel för en tjänst som måste bekräfta en summa innan den levererar, och fel för CSV-export, som skriver formeltexten snarare än dess resultat. Vilket fall som helst måste utvärderas på servern med den inbyggda motorn:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX-fasaden: inget '='-prefix
Total := BookX.Calculate('SUM(A1:A2)');       // utvärdera på servern, nu
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

Fasadkonventionen biter igen här. XLSX-sidan tilldelar uttryck via Cell.Formula utan likhetstecken; XLS-sidan skriver dem via Cell.Value med ett inledande '='. Bär kod från den ena till den andra oförändrad och den fel konventionen lagrar en textsträng som bara liknar en formel, utan något fel som flaggar det. När en arbetsboks formler behöver nå in i din egen affärslogik låter callbacken OnUserFunction motorn lämna okända funktionsnamn till Delphi-kod vid utvärderingstillfället. Det är den nativa ersättningen för de UDF-tillägg som tenderar att gömma sig inuti just de kalkylblad ett COM-automationssystem växte upp kring

Driftsättningskanter som bara syns på servern

Några detaljer avgör om utrullningen är ren eller förbryllande, och den första är enhetsgrafen. Dra-och-släpp-datasetexportören TDataToXLS drar in VCL Forms, Controls och Dialogs. Ofarligt i ett skrivbordsverktyg; i en konsoltjänst släpar den med hela VCL bakom sig. Kärnenheterna lxHandle och lxHandleX når bara efter Windows, Classes, SysUtils och Variants, så en ren tjänst gör bättre i att skriva sin egen datasetloop mot kärn-API:et än att importera komponenten av bekvämlighet

Sedan finns trådning. Arbetsboksinstanser är inte trådsäkra, men de delar inte heller något globalt tillstånd, så mönstret som skalar är det enklaste: ett arbetsboksobjekt per jobb, eller per arbetartråd. Det köper parallell rapportgenerering, vilket en enda delad Excel-instans aldrig kan göra. En förfrågningshanterare som skapar, fyller, sparar och frigör sin egen arbetsbok behöver inga lås alls, och strålningsradien för ett fel kollapsar från "den delade Excel-instansen är fastkörd för alla" ner till "den här enskilda förfrågan kastade ett undantag," vilket din befintliga felhantering redan vet vad den ska göra med

Formatmålsättning är den sista av dem. TXLSWorkbook.SaveAs skriver BIFF (xlExcel97) som standard, och att pressa XLS-innehåll in i .xlsx går via bron SaveXLSWorkbookAsXLSX med reducerad fidelitet. Välj fasad efter formatet du avser att leverera, vid designtillfället, snarare än att bygga i ett och konvertera i slutet av pipelinen

För dataladdningshalvan av ett typiskt ersättningsprojekt täcker mönstren för databas-till-arbetsbok-export både komponenten och den handskrivna loopen, och när radantalen når sex siffror blir teknikerna för prestanda hos stora arbetsböcker skillnaden mellan minuter och sekunder. Rapporter byggda från designerunderhållna layouter täcks i genomgången av mallbaserad rapportgenerering

HotXLS levereras som Object Pascal-källkod för Delphi och C++Builder; utgåvor, licensiering och den fullständiga API-referensen finns på produktsidan för HotXLS Delphi Component