Article technique

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

L'alerte tombe à 02:10. Le job de reporting nocturne est bloqué, et le Gestionnaire des tâches du serveur affiche onze processus EXCEL.EXE appartenant au compte de service, chacun attendant une boîte de dialogue sur laquelle personne ne cliquera jamais. Quiconque a piloté Excel par automatisation COM depuis un serveur a vécu une variante de cette nuit. Les propres recommandations de Microsoft sont claires depuis deux décennies : Office n'est ni conçu ni licencié pour une automatisation serveur sans surveillance. HotXLS existe précisément pour cette faille — c'est une bibliothèque native Object Pascal qui lit et écrit directement les formats de fichiers tableur, si bien qu'aucun processus Excel ne peut se bloquer, fuir ou réclamer une licence.

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.

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.

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;          // interface reference: released automatically
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // plain object: you free it
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls output - no Free; the interface refcount owns it
  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 output - explicit lifetime
  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);          // writes from the CURRENT stream position
  Mem.Position := 0;         // rewind before handing the stream over
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // the framework now owns 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é.

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)';   // XLSX facade: no '=' prefix
Total := BookX.Calculate('SUM(A1:A2)');       // evaluate on the server, now
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 Component.