Műszaki cikk

Excel fájlok Delphiben Office automatizálás nélkül

Ha egy kiszolgáló egyetlen dolga Excel fájlok kibocsátása, semmi keresnivalója nincs az Excel futtatásának. Az Office telepítése egy fordítóügynökre vagy jelentésszolgáltatásra azért, hogy COM automatizáláson át vezérelje, rossz tervezés, és pontosan addig volt rossz tervezés, amíg ez a gyakorlat létezik. Ezt maga a Microsoft is így mondja, olyan útmutatásban, amely húsz év alatt sem puhult: az Office-t nem arra építették és nem arra licencelik, hogy felügyelet nélküli, kiszolgálóoldali folyamatból automatizálják. A helyes válasz a BIFF és OOXML bájtok közvetlen kiírása, mindenféle Excel nélkül. Ez a HotXLS teljes előfeltevése, azé a natív Object Pascal könyvtáré, amely maga olvassa és írja a táblázatformátumokat, így nincs asztali alkalmazás, amely megakadhat, szivároghat vagy férőhelyenként fizettethet

Miért bukik meg az EXCEL.EXE vezérlése egy szolgáltatásból

A COM automatizálás egy asztali programot távirányít, egy asztali program pedig csendben feltételez három dolgot, amelyet egy Windows szolgáltatás nem tud átadni neki: betöltött felhasználói profilt, interaktív ablakállomást és egy embert, aki a képernyőt nézi. Vegye el ezeket, és a hibák olyan formában érkeznek, amelyet egyetlen fejlesztői gép sem reprodukál. Egy fájl-helyreállítási kérdés, egy bővítményhiba vagy egy licencaktiválási párbeszédpanel nyílik meg egy olyan asztalon, amelyet senki nem lát, az azt kiváltó automatizálási hívás pedig soha nem tér vissza. A hívó végül időtúllépéssel elhal; az Excel példány gyakran nem, hanem árvaként él tovább, fájlzárolásokat tart, és megmérgezi a következő futást. Aki már látott tizenegy elkóborolt EXCEL.EXE folyamatot feltorlódni egy szolgáltatásfiók alatt, ismeri a történet többi részét

Ábra, amely szembeállítja az EXCEL.EXE folyamatot COM automatizáláson át vezérlő Delphi szolgáltatást, ahol a rejtett párbeszédpanelek és árva folyamatok megakasztják a hívásokat, azzal, ahogy a HotXLS a BIFF8 és OOXML munkafüzetbájtokat közvetlenül, a folyamaton belül írja
A COM automatizálás örökli egy asztali program hiányzó feltevéseit, míg a HotXLS közvetlenül írja a BIFF8 és OOXML bájtokat, semmit nem kell telepíteni a kiszolgálóra

A skálázódás története sem jobb akkor, ha semmi nem omlik össze. Egy Excel példány egyetlen munkafüzetet kezelő futószalag, minden tulajdonság-hozzáférés megfizeti a folyamatok közötti COM csomagolás árát, a kódot futtató gép pedig olyan Office licencet hordoz, amelynek feltételei épp ezt a felhasználást zárják ki. A legtöbb csapat egy-egy üzemzavaronként ismerkedik meg ezekkel a korlátokkal, és nagyjából így kerül a „COM réteg nyugdíjazása” egy ütemtervre

Mielőtt ez az újraírás elindul, tisztázzon egy hatókörkérdést, mert az dönti el, mennyi a munkából a valódi. A COM kód szinte soha nem csak cellaértékeket állít be. Meghívja a Workbook.SaveAs metódust formátumkonstansokkal, újraszámolást kényszerít, nyomtatási beállítást tol, néha a vágólaphoz nyúl. Járja végig a régi kódot, és írja le, e viselkedések közül melyik jelenik meg ténylegesen a kimenetben, hiszen mindegyik egy natív könyvtár más sarkába érkezik, és néhányuknak (a vágólapos átjárás a nyilvánvaló) semmilyen kiszolgálóoldali jelentése nincs, ezért átvitel helyett el kell hagyni őket

Két natív motor, két birtoklási modell

A HotXLS az Excel folyamatot két közvetlen formátummegvalósításra cseréli. Egy BIFF8 rekordfolyam-motor (TXLSWorkbook, lxHandle unit) kezeli az .xls fájlokat. Egy OOXML csomagíró (TXLSXWorkbook, lxHandleX unit) állítja elő az ECMA-376 / ISO/IEC 29500 szerinti .xlsx fájlokat. Semmit nem kell regisztrálni és semmit nem kell telepíteni a kiszolgálóra, és annyi munkafüzetet tarthat egyszerre nyitva, amennyit a memória enged

Ábra a két HotXLS Delphi homlokzat összehasonlításáról: a TXLSWorkbook az IXLSWorkbook felület hivatkozásszámlálásán át szabadul fel automatikusan, a TXLSXWorkbook pedig sima objektum, amely kifejezett Free hívást igényel egy try..finally blokkban
Az XLS homlokzatot a felület hivatkozásszámlálása szabadítja fel, míg az XLSX homlokzat kifejezett Free hívást kíván, a lapgyűjtemények pedig az 1-alapú Entries és a 0-alapú Items között térnek el

Ami korán megbuktatja az embereket, az az, hogy a két homlokzat másként birtokolja a memóriáját, és a különbség egészen az összeomlásig néma:

var
  Book: IXLSWorkbook;          // felülethivatkozás: automatikusan felszabadul
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // sima objektum: Ön szabadítja fel
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls kimenet - nincs Free; a felület hivatkozásszámlálása birtokolja
  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 kimenet - kifejezett élettartam
  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;

Az XLS homlokzat az IXLSWorkbook felületen át hivatkozásszámlált. A változót a felület típusaként deklarálja, és soha ne hívjon rá Free metódust; ha ugyanazt az objektumot sima objektumváltozóban tartja és maga szabadítja fel, a hivatkozásszámláló másodszor is felszabadítja. Az XLSX homlokzat közönséges objektum, amely közönséges try..finally szerkezetet kíván. A cellacímzés mindkét oldalon 1-alapú, ez az egyetlen pont, ahol a kettő egyetért. A lapgyűjtemények nem: az XLS oldali Entries 1-alapú, az XLSX Items indexelő 0-alapú, és ez az eggyel elcsúszás akárhogy is rontja el, tisztán lefordul, és csak futásidőben mutatja meg magát

Munkafüzet írása egyenesen egy HTTP válaszba

Egy kiszolgálóoldali exportnak rendszerint semmi oka a lemezhez nyúlni. Az ideiglenes fájlok takarítási szabályzatot követelnek, párhuzamos kérések alatt ütköznek, és olyan köteteken hagynak ügyféladatot, amelyek ellenőrzésére senki nem gondolt. Mindkét homlokzat elfogad TStream objektumot a SaveAs túlterhelésein át, így a munkafüzet egyenesen a válaszba kerülhet:

Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // az AKTUÁLIS adatfolyam-pozíciótól ír
  Mem.Position := 0;         // tekerje vissza az adatfolyam átadása előtt
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // a Mem mostantól a keretrendszeré
finally
  Book.Free;
end;

A visszatekerés az a sor, amely kiérdemli a megjegyzését. A SaveAs(Stream) az adatfolyam aktuális pozíciójától ír, és utána soha nem keres vissza nullára. Felejtse el a Mem.Position := 0 sort, és az ügyfél nulla bájtos letöltést kap, vagy az Excel sérültnek mondja a fájlt. Ez a leggyakoribb hiba a webre néző munkafüzetkódban, és a legkegyetlenebb, mert átvitorlázik minden olyan egységteszten, amely csak azt állítja, hogy az adatfolyam hossza nem nulla

Egyetlen munkafüzetépítő rutin átrendezés nélkül elér minden más kézbesítési formátumot. A SaveAsCSV a „csak add ide a nyers adatot” kérésre válaszol, a SaveAsHTML a „dobd be egy portáloldalra” esetet kezeli, a SaveAsRTF dokumentumfolyamatokat táplál, a SaveAsODS pedig egy OpenDocument előírást fed le, mindegyik fájl- és adatfolyamos túlterheléssel. Egyetlen exportrutin plusz egy formátumparaméter váltja ki azt, ami korábban négy külön COM makró szokott lenni. A HTML exportáló TXLSXHtmlExportOptions objektuma címet, CSS osztályt és egy töredék-vagy-teljes-dokumentum kapcsolót hordoz, ami kiveszi a portálesetet az exportált jelölés reguláris kifejezéses szerkesztgetésének üzletágából

Ábra egy Delphi kéréskezelőről, amely egy HotXLS munkafüzetet TMemoryStream objektumba ment, a Mem.Position értéket nullára tekeri vissza, és átadja az adatfolyamot a HTTP válasznak, mellette a CSV, HTML, RTF és ODS exportálókkal
A TMemoryStream objektumba mentés és az átadás előtti visszatekerés egyenesen az ügyfélhez küldi a munkafüzet bájtjait, egyetlen exportrutin pedig lefedi a CSV, HTML, RTF és ODS írókat

Képletértékek anélkül, hogy egy Excel folyamat számolná ki őket

COM automatizálás alatt az Excel ingyen újraszámolt mindent, és a COM elhagyása ezt csendben visszavonja. A SaveAs kiértékelés nélkül, szövegként tárolja a képleteket; a számok csak akkor jelennek meg, amikor az Excel megnyitja a fájlt és újraszámol, ezt a viselkedést pedig az XLS homlokzat a RecalcOnSave és a CalculationMode tagokkal engedi hangolni. Egy embernek szánt fájlnál ez pontosan helyes. Helytelen viszont egy olyan szolgáltatásnál, amelynek szállítás előtt igazolnia kell egy végösszeget, és helytelen a CSV exportnál, amely a képlet szövegét írja ki az eredménye helyett. Mindkét esetben a kiszolgálón kell kiértékelni a beépített motorral:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX homlokzat: nincs '=' előtag
Total := BookX.Calculate('SUM(A1:A2)');       // kiértékelés a kiszolgálón, most
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

A homlokzategyezmény itt megint harap. Az XLSX oldal a Cell.Formula tagon át, egyenlőségjel nélkül adja meg a kifejezéseket; az XLS oldal a Cell.Value tagon át írja őket, kezdő '=' jellel. Vigye át a kódot változatlanul az egyikről a másikra, és a rossz egyezmény olyan szöveges karakterláncot tárol, amely pusztán képletre hasonlít, minden jelző hiba nélkül. Amikor egy munkafüzet képleteinek a saját üzleti logikájába kell nyúlniuk, az OnUserFunction visszahívás engedi, hogy a motor kiértékeléskor Delphi kódnak adja át az ismeretlen függvényneveket. Ez a natív helyettesítője azoknak a UDF bővítményeknek, amelyek épp azokban a táblázatokban szoktak megbújni, amelyek köré egy COM automatizálású rendszer felnőtt

Telepítési peremek, amelyek csak a kiszolgálón bukkannak fel

Néhány részlet dönti el, hogy a bevezetés tiszta vagy rejtélyes lesz, és az első a unitgráf. A beejthető adatkészlet-exportáló TDataToXLS behúzza a VCL Forms, Controls és Dialogs egységeit. Egy asztali eszközben ez ártalmatlan; egy konzolos szolgáltatásban maga után vonszolja az egész VCL-t. Az lxHandle és lxHandleX magegységek csak a Windows, Classes, SysUtils és Variants egységekhez nyúlnak, ezért egy tiszta szolgáltatás jobban jár, ha saját adatkészlet-ciklust ír a mag API ellen, mintha kényelmi okból importálná a komponenst

Aztán ott a szálkezelés. A munkafüzet-példányok nem szálbiztosak, de közös globális állapotuk sincs, így a skálázódó minta a legegyszerűbb: feladatonként vagy munkaszálanként egy munkafüzet-objektum. Ez párhuzamos jelentéskészítést vesz, amit egyetlen közös Excel példány sosem tud. Az a kéréskezelő, amely a saját munkafüzetét hozza létre, tölti fel, menti és szabadítja fel, semmilyen zárat nem igényel, egy hiba robbanási sugara pedig a „a közös Excel példány mindenki számára beszorult” szintről a „ez az egy kérés kivételt váltott ki” szintre zsugorodik, amivel a meglévő hibakezelése már tudja, mit kezdjen

A formátum célzása az utolsó közülük. A TXLSWorkbook.SaveAs alapértelmezés szerint BIFF (xlExcel97) formátumot ír, az XLS tartalom .xlsx felé tolása pedig a SaveXLSWorkbookAsXLSX hídon át, csökkentett hűséggel megy. A homlokzatot a szállítani kívánt formátum szerint válassza, tervezéskor, ne az egyikben építsen, hogy a folyamat végén átalakítson

Egy tipikus kiváltó projekt adatbetöltő feléhez az adatbázisból munkafüzetbe vezető exportminták mind a komponenst, mind a kézzel írt ciklust lefedik, és amint a sorszám hatjegyűvé válik, a nagy munkafüzetek teljesítménytechnikái jelentik a percek és a másodpercek közötti különbséget. A tervezők által karbantartott elrendezésekből épülő jelentéseket a sablonalapú jelentéskészítés végigvezetése tárgyalja

A HotXLS Object Pascal forrásként érkezik Delphihez és C++Builderhez; a kiadások, a licencelés és a teljes API hivatkozás a HotXLS Delphi Component termékoldalán található