Article technique

HotXLS : préserver macros VBA et liens externes en Delphi

Imaginez une tâche qui ne fait presque rien : ouvrir un classeur mensuel, écrire la date du jour dans une cellule, l'enregistrer. Faites-la tourner assez souvent dans un service et une réclamation finit par arriver. Les macros ont disparu, ou les taux de change liés affichent désormais #REF!, et l'équipe d'exploitation est convaincue que votre code les a supprimés. Il n'a rien supprimé. Ce qui s'est passé, la plupart du temps, c'est qu'un classeur à macros est reparti sous un simple nom .xlsx, et Excel a appliqué les règles de type de contenu d'ECMA-376 : un paquet dont le type de contenu ne déclare aucun VBA ne peut pas charger de projet VBA, que les octets soient là ou non. Le fichier n'est pas cassé. Il a été renommé vers un état où Excel a l'obligation d'en ignorer une partie

Les macros et les liens vers des classeurs externes sont les deux éléments que l'automatisation perd le plus sûrement, pour la même raison de fond. Tous deux vivent en dehors de la grille de cellules que le code d'édition touche réellement, si bien qu'un code qui raisonne en lignes et en colonnes les laissera tomber sans jamais émettre la moindre suppression. HotXLS est une bibliothèque native pour Delphi et C++Builder qui lit et écrit les formats XLS et XLSX sans Excel installé, et elle traite ces deux ressources comme des charges utiles transportées délibérément plutôt que comme des données recopiées par hasard. Voici ce que chacune exige de votre chemin de sauvegarde, et où les garanties s'arrêtent

Pourquoi ces deux ressources se comportent différemment lors d'une réécriture

Un projet VBA est un binaire opaque unique. Dans un paquet OOXML, c'est le fichier vbaProject.bin ; dans un fichier BIFF hérité, c'est un stockage OLE. Il existe exactement deux façons de le perdre : le module d'écriture ne le recopie jamais dans la sortie, ou la sortie reçoit un type de fichier qui l'interdit. Dans les deux cas, l'échec est total et silencieux. Le projet est présent ou il ne l'est pas

Un lien externe n'est pas du tout un bloc binaire. C'est un petit graphe de relations : un chemin ou une URL cible pointant vers un autre classeur, la liste des noms de feuilles que cette cible expose, et un cache facultatif des dernières valeurs vues dans ces feuilles pour qu'Excel puisse afficher quelque chose lorsque la cible est hors ligne. Ces trois parties ont des durées de vie différentes lors d'une réécriture, et une bibliothèque peut en préserver fidèlement certaines tout en abandonnant les autres sans bruit. Cette asymétrie mérite d'être précisée, car rien dans le code d'édition des cellules ne la fera remonter

Diagramme comparant le bloc binaire d'un projet VBA aux trois parties d'un lien vers un classeur externe que HotXLS transporte lors d'une réécriture en Delphi
Un projet VBA survit à une réécriture comme une charge binaire tout ou rien, tandis qu'un lien externe est un petit graphe dont la cible, les noms de feuilles et les valeurs en cache peuvent être conservés ou abandonnés indépendamment

Transporter un projet VBA à travers une réécriture XLSX

Côté XLSX, TXLSXWorkbook conserve la charge utile des macros telle quelle. La propriété VbaProject contient les octets bruts de vbaProject.bin dans une AnsiString, et une chaîne vide est la façon dont le modèle indique qu'il n'y a pas de macros. Autour d'elle se placent trois opérations : HasVbaProject répond si un projet est présent, ClearVbaProject le supprime volontairement, et LoadVbaProjectFromFile en injecte un extrait d'un modèle. Ce dernier appel vaut plus qu'il n'y paraît. Il permet à des classeurs générés de reprendre un projet de macros standard sans traîner un fichier modèle complet dans toute la chaîne

Diagramme de flux d'un appel de sauvegarde en Delphi où l'extension .xlsm sélectionne le type de contenu à macros activées tandis que .xlsx fait qu'Excel refuse silencieusement les macros
HotXLS transporte les octets bruts de vbaProject.bin jusqu'à la sauvegarde, et c'est l'extension .xlsm qui sélectionne le type de contenu à macros activées exigé par Excel
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Data');
    Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);

    Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
    if not Book.HasVbaProject then
      raise Exception.Create('VBA payload failed to load');

    // L'extension .xlsm n'est pas cosmétique : elle sélectionne le
    // type de contenu à macros activées à l'intérieur du paquet.
    Book.SaveAs('monthly-report.xlsm');
  finally
    Book.Free;
  end;
end;

La ligne de sauvegarde est le point où tout le problème bascule. Un classeur porteur d'un projet VBA doit être écrit avec la sémantique des macros activées, et HotXLS applique celle-ci lorsque le nom cible se termine par .xlsm. Donnez-lui .xlsx à la place et Excel refuse les macros, alors même que les octets sont physiquement présents dans le paquet et se désérialiseraient sans problème. L'extension n'est pas décorative ; elle sélectionne le type de contenu qui indique à Excel qu'un projet VBA a le droit d'exister. La plupart du temps, il vous suffit de transporter la charge utile. Lorsque vous devez y lire, par exemple pour lister les noms de modules dans un rapport d'audit, ParsedVBAProject expose un modèle de modules analysé tandis que VbaProject reste les octets d'origine intacts

Réutiliser les macros de classeurs XLS hérités

La façade BIFF reflète cette panoplie avec une étape supplémentaire. HasVBAProject sonde un fichier chargé, SaveVBAProjectToFile écrit le stockage du projet sur disque, et LoadVBAProjectFromFile le relit dans un autre classeur. Ce détour par un fichier rend simple une corvée de modernisation courante : extraire les macros d'un modèle de l'époque 2003 et les implanter dans une sortie XLS fraîchement générée, sans avoir besoin du modèle d'origine à l'exécution

var
  Src, Dst: IXLSWorkbook;   // références d'interface : pas de Free manuel
begin
  Src := TXLSWorkbook.Create;
  if Src.Open('legacy-model.xls') <= 0 then
    raise Exception.Create('Cannot open legacy model');
  if Src.HasVBAProject then
    Src.SaveVBAProjectToFile('extracted-vba.bin');

  Dst := TXLSWorkbook.Create;
  Dst.Sheets.Add.Name := 'Report2026';
  Dst.LoadVBAProjectFromFile('extracted-vba.bin');
  Dst.SaveAs('report-with-macros.xls');
end;

Le modèle mémoire est le piège ici, et il fonctionne à l'inverse de la classe XLSX. TXLSWorkbook se manipule via l'interface à comptage de références IXLSWorkbook, si bien que vous ne le libérez jamais à la main ; le TXLSXWorkbook du côté XLSX est un objet ordinaire que vous devez encadrer d'un try..finally et libérer. Mélangez les deux conventions dans une même unité et les plantages par double libération suivent. Une autre frontière mérite le respect : gardez extraction et injection à l'intérieur d'un seul format de fichier. Le stockage de projet BIFF et le vbaProject.bin OOXML sont cousins, pas le même conteneur, et une chaîne qui doit émettre des macros dans les deux formats devrait conserver un modèle de macros distinct pour chacun

Liens externes : la carte survit, les valeurs en cache non

Pour les classeurs XLSX, HotXLS expose les liens externes par la collection ExternalLinks. Chaque TXLSXExternalLink porte un Target, le chemin ou l'URL du classeur distant, plus une liste SheetNames nommant les feuilles qu'il référence. Tous deux survivent intacts à un cycle ouverture-sauvegarde, et vous pouvez aussi construire un lien de toutes pièces :

var
  Link: TXLSXExternalLink;
begin
  Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
  Link.SheetNames.Add('FX');

  if Book.ExternalLinks.Count > 0 then
    Writeln(Format('%d external link(s): delivery requires reachable targets',
      [Book.ExternalLinks.Count]));
end;

La frontière se situe un niveau plus bas que la liste des cibles. HotXLS fait circuler la carte des liens, c'est-à-dire la cible et les noms de feuilles, mais il n'analyse ni ne réécrit les valeurs de cellules en cache que OOXML garde dans l'élément sheetDataSet du lien. Ce cache est ce qui permet à Excel d'afficher un dernier nombre connu lorsque le fichier source est hors ligne, et un classeur généré part sans lui. La conséquence retombe sur le destinataire, pas sur vous. Ouvrez un tel fichier là où la cible est injoignable, un portable hors VPN ou un partage renommé, et les formules qui dépendent du lien se résolvent en #REF! ou restent bloquées derrière une invite de mise à jour. Deux règles en découlent. Ne promettez pas qu'un classeur généré affichera ses valeurs liées hors ligne. Et lisez un ExternalLinks.Count non nul comme une condition préalable de livraison plutôt que comme une fonctionnalité : chaque cible doit être joignable depuis l'endroit où le fichier sera réellement ouvert

Diagramme des parties d'un lien HotXLS vers un classeur externe qui survivent à une réécriture et de ce qui se passe hors ligne lorsque les valeurs en cache derrière sheetDataSet sont absentes
HotXLS fait circuler la cible du lien et ses noms de feuilles, mais les valeurs de cellules en cache derrière sheetDataSet ne sont pas reportées dans le fichier généré

Ce que le lecteur XLS préserve octet par octet

Pour les structures qu'il ne modélise pas, le côté BIFF apporte une réponse différente : les laisser exactement telles qu'elles ont été trouvées. Les caches et les vues de tableaux croisés dynamiques (la famille d'enregistrements SX*), les définitions de QueryTable, les connexions de données externes, les vues personnalisées, les images d'en-tête et les enregistrements de thème traversent tous un cycle ouverture-sauvegarde sous forme de blocs d'enregistrements bruts, non analysés et non modifiés. Les références externes elles-mêmes font l'aller-retour par les enregistrements EXTERNSHEET et SupBook sous-jacents. Il n'existe pas d'API typée pour les créer côté XLS, mais un lien existant survit intact à l'édition

La préservation octet par octet est une vraie garantie, avec un tranchant. Comme rien ne lit une structure préservée, vos modifications ne peuvent pas la corrompre. Pour la même raison, rien ne la met à jour non plus. Insérez des lignes en travers d'une zone que vise un cache de tableau croisé ou une table de requête préservée, et la structure garde ses coordonnées d'origine pendant que les données en dessous se décalent. Le fichier reste un XML ou un BIFF valide ; le sens, lui, a discrètement glissé hors alignement, et aucune erreur ne se déclenche pour vous le dire. La disposition défendable consiste à garder les modifications générées sur des feuilles qui ne portent aucune structure préservée, la même discipline que celle qui protège les feuilles verrouillées et configurées pour l'impression dans notre article sur la protection des feuilles et la mise en page

Vérifier le fichier que vous avez réellement écrit

Les deux modes de défaillance sont silencieux à l'écriture, si bien que l'assertion qui compte se fait en rouvrant la sortie plutôt qu'en faisant confiance au code qui l'a produite. Trois contrôles couvrent presque tout. Rouvrez le fichier et confirmez que HasVbaProject renvoie toujours true chaque fois que des macros étaient attendues, ce qui attrape en un seul test une charge utile perdue et une mauvaise extension. Lisez ExternalLinks.Count et comparez-le au décompte relevé avant la réécriture. Puis ouvrez une fois le fichier dans Excel, macros désactivées, car la validation du type de contenu par Excel est plus stricte que celle de n'importe quelle bibliothèque, et Excel est le programme sur lequel vos clients jugeront le fichier

Rien de tout cela n'exige une analyse complète à l'entrée. Lorsque les classeurs arrivent en volume et que vous devez seulement trier ceux qui portent du contenu réglementé, le sondage léger décrit dans notre article sur le listage des feuilles et l'inspection légère des classeurs vous permet d'aiguiller les fichiers porteurs de macros et de liens vers une chaîne plus stricte avant que la première réécriture ne démarre

Quelques questions reviennent assez souvent pour mériter une réponse directe. HotXLS n'exécute jamais les macros qu'il préserve : il n'y a pas de moteur VBA dans la bibliothèque, seulement la mécanique permettant de stocker, copier, extraire et injecter le projet en tant que données. Sur un serveur, c'est une propriété de sécurité qui mérite d'être énoncée, puisqu'une macro hostile traversant la chaîne reste inerte jusqu'à ce qu'un Excel de bureau ouvre le fichier et qu'un utilisateur active le contenu. Convertir un .xlsm en .xlsx en gardant les macros est impossible, et c'est la règle du format plutôt qu'une limite de la bibliothèque : le type de contenu .xlsx déclare un classeur sans macros, si bien que les seuls dénouements honnêtes sont de rester en .xlsm ou d'appeler ClearVbaProject et de livrer un fichier qui n'en a véritablement aucune. Le renommage silencieux est le seul choix qui ne satisfait personne. Et lorsque des cellules liées affichent #REF! après une réécriture, la cause est le cache de valeurs manquant évoqué plus haut : le nouveau fichier porte la cible mais pas les nombres mis en cache, si bien qu'Excel doit résoudre la source à l'ouverture, et un chemin injoignable ou dépendant de l'environnement le met en échec. Soit vous garantissez que la cible est joignable, soit vous écrivez les valeurs calculées dans les cellules avant la livraison et supprimez complètement la dépendance

Modifier les classeurs des autres consiste surtout à préserver des choses que vous n'avez pas écrites et que vous ne comprenez pas entièrement. Les fonctions d'aller-retour pour VBA et liens externes décrites ici sont livrées avec le HotXLS Delphi Component pour Delphi et C++Builder, ainsi que les propriétés d'audit qui vous permettent de détecter le contenu réglementé dès qu'un fichier arrive