Article technique

Générer des fichiers Excel dans Delphi sans Office Automation

Si le seul travail d'un serveur est de produire des fichiers Excel, il n'a rien à faire tourner Excel. Installer Office sur un agent de build ou un service de reporting pour le piloter en automatisation COM est une mauvaise conception, et l'est depuis que la pratique existe. Microsoft le dit lui-même, dans des recommandations qui n'ont pas faibli en vingt ans : Office n'est ni conçu ni licencié pour être automatisé depuis un processus serveur sans surveillance. La bonne réponse est d'écrire directement les octets BIFF et OOXML, sans aucun Excel dans le tableau. C'est toute la prémisse de HotXLS, une bibliothèque Object Pascal native qui lit et écrit elle-même les formats de tableur : plus d'application de bureau qui se bloque, qui fuit ou qui se paie au poste

Pourquoi piloter EXCEL.EXE depuis un service échoue

L'automatisation COM télécommande une application de bureau, et une application de bureau suppose des choses qu'un service Windows ne fournit pas : un profil utilisateur chargé, une station de fenêtre interactive et un humain qui regarde l'écran. Sous un compte de service, ces hypothèses se brisent d'une manière qui n'apparaît jamais sur une machine de développement. Une invite de récupération de fichier, une erreur d'add-in ou une boîte d'activation de licence apparaît sur un bureau que personne ne voit, et l'appel d'automatisation ne revient tout simplement jamais. Lorsque le processus appelant abandonne et meurt, l'instance Excel survit souvent comme orpheline, gardant des verrous de fichiers qui cassent aussi l'exécution suivante

Diagramme contrastant un service Delphi pilotant EXCEL.EXE via l'automatisation COM, où dialogues cachés et processus orphelins bloquent les appels, avec HotXLS écrivant directement les octets de classeurs BIFF8 et OOXML dans le processus
L'automatisation COM hérite des hypothèses manquantes d'un programme de bureau, tandis qu'HotXLS écrit directement les octets BIFF8 et OOXML sans rien à installer sur le serveur

Même lorsque rien ne plante, le modèle passe mal à l'échelle. Chaque instance Excel est en pratique une chaîne mono-classeur, le marshalling COM interprocessus ajoute de la latence à chaque accès de propriété, et la machine qui exécute le code a besoin d'une licence Office dont l'EULA ne couvre pas cet usage. Les équipes découvrent typiquement chacune de ces contraintes incident après incident, raison pour laquelle « remplacer la couche COM » finit par arriver dans le plan trimestriel de quelqu'un

Le remplacement porte une question de périmètre cachée qu'il vaut mieux régler tôt : le code COM écrit rarement seulement des cellules. Il appelle Workbook.SaveAs avec des constantes de format, déclenche un recalcul, applique une mise en page d'impression et pilote parfois le presse-papiers. Inventoriez ces comportements avant la réécriture, car chacun correspond à une partie différente de l'API d'une bibliothèque native — et quelques-uns, comme l'interop presse-papiers, n'ont pas d'équivalent serveur et ne doivent pas être reproduits

Deux moteurs natifs, deux modèles de possession

HotXLS remplace le processus Excel par des implémentations directes de formats : un moteur de flux d'enregistrements BIFF8 (TXLSWorkbook, unité lxHandle) pour .xls, et un writer de package OOXML (TXLSXWorkbook, unité lxHandleX) qui produit du .xlsx conforme à ECMA-376 / ISO/IEC 29500. Rien à enregistrer, rien à installer sur le serveur, et autant de classeurs en vol que la mémoire le permet

Diagramme comparant les deux façades HotXLS Delphi : TXLSWorkbook libéré automatiquement via le comptage de références de l'interface IXLSWorkbook, et TXLSXWorkbook comme objet simple nécessitant un Free explicite dans un bloc try..finally
La façade XLS se libère par comptage de références d'interface tandis que la façade XLSX exige un Free explicite, et les collections de feuilles diffèrent entre Entries en base un et Items en base zéro

La première chose à internaliser est que les deux façades suivent des règles de durée de vie différentes, et les confondre est le bug classique de première semaine :

var
  Book: IXLSWorkbook;          // référence d'interface : libérée automatiquement
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // objet ordinaire : vous le libérez
  SheetX: TXLSXWorksheet;
begin
  // sortie .xls BIFF8 - pas de Free ; le refcount d'interface la possède
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // sortie .xlsx OOXML - durée de vie explicite
  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;

La façade XLS est comptée par références via l'interface IXLSWorkbook : déclarez la variable comme type interface et n'appelez jamais Free, car la garder dans une variable objet puis la libérer manuellement prépare une double libération. La façade XLSX est un objet ordinaire avec un try..finally ordinaire. L'adressage des cellules est 1-based des deux côtés, mais les collections de feuilles diffèrent — Entries côté XLS est 1-based tandis que l'indexeur Items côté XLSX est 0-based, un off-by-one qui compile proprement dans les deux sens et n'échoue qu'à l'exécution

Écrire un classeur directement dans une réponse HTTP

Les exports serveur ne devraient généralement pas toucher au disque. Les fichiers temporaires ont besoin d'une politique de nettoyage, entrent en collision entre requêtes concurrentes et laissent des données client sur des volumes que personne n'audite. Les deux façades exposent des surcharges SaveAs prenant un TStream :

Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // écrit depuis la POSITION ACTUELLE du flux
  Mem.Position := 0;         // rembobiner avant de remettre le flux
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // le framework possède désormais Mem
finally
  Book.Free;
end;

La ligne qui mérite son commentaire est le rembobinage. SaveAs(Stream) écrit depuis la position courante du stream et ne revient pas à zéro lorsqu'il termine. Oubliez Mem.Position := 0 et le client reçoit un téléchargement de zéro octet ou Excel signale un fichier corrompu — de très loin l'erreur la plus courante dans le code de classeur exposé au web, et une erreur qui passe tous les tests unitaires vérifiant seulement la taille du stream

Le même code de génération se décline vers d'autres formats de livraison sans restructuration. SaveAsCSV et SaveAsHTML couvrent les demandes « donnez-moi juste les données » et « intégrez-le dans une page portail » qui suivent toujours un export Excel réussi, SaveAsRTF alimente les chaînes documentaires, et SaveAsODS satisfait les mandats OpenDocument — chacun avec des surcharges fichier et stream. Un service qui expose une routine de construction de classeur et un paramètre de format remplace ce qui était autrefois quatre macros COM différentes, et les TXLSXHtmlExportOptions de l'exporteur HTML (titre, classe CSS, sortie fragment ou document) évitent que le cas portail ne dégénère en chirurgie de chaînes sur du balisage exporté

Diagramme d'un gestionnaire de requêtes Delphi enregistrant un classeur HotXLS dans un TMemoryStream, ramenant Mem.Position à zéro, et remettant le flux à la réponse HTTP, avec les exportateurs CSV, HTML, RTF et ODS à côté
Enregistrer dans un TMemoryStream puis le rembobiner avant la remise envoie les octets du classeur directement au client, et une seule routine d'export couvre les rédacteurs CSV, HTML, RTF et ODS

Valeurs de formules sans processus Excel pour les calculer

Avec l'automatisation COM, Excel recalculait tout gratuitement. Un writer natif change le contrat : SaveAs stocke les formules sans les évaluer, et les valeurs apparaissent quand Excel ouvre le fichier et recalcule (la façade XLS expose RecalcOnSave et CalculationMode pour régler ce comportement). C'est parfait pour les fichiers destinés à un humain, mais un service qui doit valider des totaux avant livraison — ou exporter en CSV, qui émet le texte de formule plutôt que les résultats — doit évaluer côté serveur avec le moteur intégré :

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // façade XLSX : pas de préfixe '='
Total := BookX.Calculate('SUM(A1:A2)');       // évaluer sur le serveur, maintenant
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

Surveillez ici aussi la convention de façade : côté XLSX, les expressions sont affectées via Cell.Formula sans signe égal, tandis que côté XLS les formules s'écrivent via Cell.Value avec un '=' initial. Portez du code entre les deux et la mauvaise convention stocke silencieusement une chaîne de texte qui ressemble seulement à une formule. Lorsque les formules de classeur doivent appeler votre propre logique métier, le callback OnUserFunction laisse le moteur de calcul dispatcher les noms de fonctions inconnus vers du code Delphi au moment de l'évaluation — l'équivalent natif le plus proche des add-ins UDF qui se cachent souvent dans les tableurs autour desquels un système d'automatisation COM a été construit

Limites de déploiement qui n'apparaissent que sur le serveur

Trois détails séparent régulièrement un déploiement propre d'un déploiement confus. D'abord, le graphe d'unités : l'exporteur de dataset glisser-déposer TDataToXLS utilise les unités VCL Forms, Controls et Dialogs, ce qui est sans conséquence dans un outil bureau mais entraîne la VCL dans un service console. Les unités cœur lxHandle et lxHandleX ne dépendent que de Windows, Classes, SysUtils et Variants, donc les services purs doivent écrire leur propre boucle dataset contre l'API cœur au lieu de tirer le composant

Ensuite, le threading : les instances de classeur ne sont pas thread-safe, mais il n'y a pas non plus d'état global partagé entre instances ; le modèle scalable est donc simplement un objet classeur par job ou par worker thread — une génération parallèle de rapports que l'automatisation COM ne pouvait jamais offrir. Un handler de requête qui crée, remplit, enregistre et libère son propre classeur n'a besoin d'aucun verrou, et le domaine de panne se réduit de « l'instance Excel partagée est bloquée » à « cette requête a levé une exception », panne que votre gestion d'erreurs sait déjà rapporter

Enfin, le ciblage de format : TXLSWorkbook.SaveAs écrit du BIFF (xlExcel97) par défaut, et transformer du contenu XLS en .xlsx passe par le pont SaveXLSWorkbookAsXLSX avec fidélité réduite. Choisissez la façade selon le format cible au moment de la conception plutôt que de convertir à la fin de la chaîne

Pour la moitié chargement de données d'un projet de remplacement typique, les modèles d'export base de données vers classeur couvrent à la fois le composant et la boucle écrite à la main ; lorsque les nombres de lignes atteignent six chiffres, les techniques de performance pour grands classeurs font la différence entre minutes et secondes. Les rapports construits depuis des mises en page maintenues par des concepteurs sont couverts dans le guide de génération de rapports par modèle

HotXLS est livré comme source Object Pascal pour Delphi et C++Builder ; les éditions, la licence et la référence complète de l'API se trouvent sur la page produit HotXLS Delphi Component