Article technique

Noms définis et formules inter-feuilles en Delphi (HotXLS)

Un nom défini est une étiquette qui tient lieu de constante, de plage de cellules ou d'expression de formule, rangée une seule fois dans le classeur et référencée symboliquement partout où elle sert. Écrivez TaxRate dans une formule et le moteur le résout vers ce que contient la définition du nom, que ce soit le littéral 0.08 ou la plage Data!$A$2:$D$100. Une référence inter-feuilles est l'idée orthogonale : Data!D2 atteint une cellule d'une autre feuille en qualifiant l'adresse par un nom de feuille. Réunissez les deux et une feuille de synthèse peut totaliser une feuille de détail par un nom qui ne mentionne jamais d'adresse littérale, ce qui est exactement ce que vous voulez dans un classeur assemblé par un générateur et audité plus tard par un comptable

HotXLS, la bibliothèque Delphi native de losLab pour les fichiers XLS et XLSX, expose la table des noms des deux formats avec un accès en création, en recherche et en suppression, ainsi qu'un moteur de formules qui résout les noms et les références inter-feuilles dans le processus. Les deux formats conservent des hiérarchies de classes distinctes, et les différences entre leurs API de noms sont ce qui fait trébucher le code porté de l'un vers l'autre

Deux magasins de noms qui ne partagent pas la même interface

Côté XLS, TXLSWorkbook.GetNames renvoie une collection IXLSNames dont la surcharge Add(Name, RefersTo, Visible) écrit un nom dans la table de noms BIFF. Les entrées individuelles reviennent sous forme d'objets IXLSName portant Name, RefersTo, une plage résolue RefersToRange et une méthode Delete. Côté XLSX, TXLSXWorkbook.DefinedNames est une collection TXLSXDefinedNames dotée de Add, FindByName et DeleteByName

Les conventions de recherche divergent d'une manière qui se révèle lors du portage plutôt qu'à la compilation. La propriété Item par défaut de la collection XLS accepte un Variant, si bien que Names[0] comme Names['TaxRate'] se résolvent sur elle. La collection XLSX n'a pas de propriété par défaut ; vous appelez FindByName('TaxRate'), qui renvoie nil quand le nom est absent. Le code écrit pour une façade ne compile sur l'autre que par accident, et la panne se manifeste plutôt par un accès nil à l'exécution que par un soulignement rouge dans l'IDE

La portée est la première décision, pas un drapeau ajouté après coup

Un nom défini est soit de portée classeur, visible par les formules de toutes les feuilles, soit de portée feuille, visible seulement par les formules de la feuille qui le possède. Dans l'API XLSX, la distinction tient à un unique paramètre facultatif. DefinedNames.Add(AName, AFormula) crée un nom au niveau du classeur, tandis que Add(AName, AFormula, ASheetIndex) le lie à une seule feuille. À la relecture, TXLSXDefinedName.SheetIndex renvoie -1 pour la portée classeur et l'index de feuille en base 0 sinon

La portée fait aussi office de politique de collision, et c'est la raison de la trancher avant d'écrire le premier nom. Excel autorise un Total local sur chaque feuille en plus d'un Total au niveau du classeur, et une formule d'une feuille donnée résout d'abord le nom local. Les classeurs générés devraient s'appuyer là-dessus délibérément. Les hypothèses métier que plusieurs feuilles consomment, comme les taux de taxe, les taux de change et la période de reporting, ont leur place à la portée classeur. Les plages auxiliaires que seules les formules d'une feuille référencent sont plus sûres en portée feuille, où rien ne peut les masquer et où elles ne masquent rien

Diagramme des noms définis de portée classeur et de portée feuille dans HotXLS, avec le paramètre de portée en Delphi et la règle de collision du nom local
Le paramètre de portée est une décision de conception : les hypothèses métier vivent à la portée classeur tandis que les auxiliaires propres à une feuille restent en portée feuille, où le nom local se résout en premier
var
  Book: TXLSXWorkbook;
  Data, Summary: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Data := Book.Sheets.Add('Data');
    Summary := Book.Sheets.Add('Summary');
    // ... remplir Data!A2:D100 avec les lignes de détail ...

    Book.DefinedNames.Add('TaxRate', '0.08');                // portée classeur, une constante
    Book.DefinedNames.Add('DataBlock', 'Data!$A$2:$D$100');  // portée classeur, une plage
    Book.DefinedNames.Add('LocalNote', 'Summary!$B$1', 1);   // limité à la feuille d'index 1

    // les formules XLSX ne prennent pas de '=' initial
    Summary.Cells[2, 2].Formula := 'SUM(Data!D2:D100)*TaxRate';
    Book.SaveAs('model.xlsx');
  finally
    Book.Free;
  end;
end;

Un nom défini n'a pas besoin de pointer vers une plage. TaxRate ci-dessus désigne la constante nue 0.08, et c'est la façon la plus propre de publier une hypothèse métier. Elle apparaît une fois dans le Gestionnaire de noms d'Excel, chaque formule la référence symboliquement, et le changement de taux du trimestre suivant est une modification d'une ligne dans le générateur au lieu d'une recherche à travers quatorze chaînes de formules assemblées

Le signe égal qui ne va que d'un seul côté

Le canal de saisie des formules est l'endroit où le code porté casse le plus souvent, car les deux façades ne s'accordent pas sur le signe égal. Les cellules XLS reçoivent les formules par Value avec un = initial. Les cellules XLSX ont une propriété Formula dédiée qui prend l'expression sans ce préfixe. Écrivez '=SUM(A1:A10)' dans TXLSXCell.Formula et le signe égal devient une partie du texte de l'expression stockée au lieu d'un marqueur, et le fichier ne se comportera pas comme la même chaîne le faisait côté XLS

Diagramme opposant les canaux de saisie de formule en Delphi dans HotXLS, où Value côté XLS exige un signe égal initial et où Formula côté XLSX l'interdit
La même expression entre par Value avec un signe égal côté XLS et par Formula sans signe égal côté XLSX — mélanger les conventions range le signe comme du texte
var
  Book: IXLSWorkbook;   // compté par interface : ne pas appeler Free
  Names: IXLSNames;
begin
  Book := TXLSWorkbook.Create;
  // on suppose qu'une feuille nommée 'Data' contient déjà les lignes de détail
  Names := Book.GetNames;
  Names.Add('TaxRate', '0.08');
  Names.Add('Helper', 'Data!$A$2:$A$100', False);  // False = masqué dans le Gestionnaire de noms

  // les formules XLS passent par Value, avec le préfixe '='
  Book.Sheets[1].Cells.Item[2, 2].Value := '=SUM(Data!A2:A100)*TaxRate';
  Book.SaveAs('model.xls');
end;

Cet extrait montre deux autres bizarreries côté XLS. La collection de feuilles est en base 1, donc Sheets[1] est la première feuille, face au Sheets[0] en base 0 du XLSX. Et le troisième paramètre de Add crée un nom masqué : présent dans le fichier et utilisable par les formules, mais invisible dans le Gestionnaire de noms d'Excel. Les noms masqués sont le bon véhicule pour la plomberie interne d'un générateur que les utilisateurs finaux ne devraient jamais modifier ni supprimer par accident

Références inter-feuilles, et ce qui arrive quand les lignes bougent

Les deux moteurs de formules acceptent la syntaxe inter-feuilles standard. Les noms de feuille simples se qualifient directement comme Data!A1 ; un nom comportant des espaces ou de la ponctuation demande des apostrophes simples, comme dans 'Sheet With Space'!A1. À l'intérieur du texte RefersTo d'un nom, optez pour des références absolues telles que Data!$A$2:$D$100 à peu près à chaque fois. Une référence relative à l'intérieur d'un nom défini se résout relativement à la cellule qui l'utilise, ce qui est une fonctionnalité Excel délibérée et une source fiable de confusion quand elle se déclenche par accident

Les modifications structurelles sont là où la comptabilité inter-feuilles gagne son salaire, et le côté XLSX garde les noms cohérents à travers elles. InsertRows et DeleteRows décalent les plages des noms définis en même temps que les cellules, les fusions, les liens hypertexte et les ancrages de graphiques, si bien qu'un nom pointant vers Data!$A$2:$D$100 couvre encore le bloc de données après que le générateur a ouvert un espace au-dessus. Les formules viennent avec une réserve documentée : l'insertion de lignes n'ajuste que les références qui visent la feuille en cours de modification. Une formule de Summary qui référence Data!D2:D100 est réécrite quand des lignes entrent dans Data, ce qui est le cas que vous voulez généralement. Vérifiez-le plutôt que de le supposer, car le moteur vous le dira à peu de frais :

// le moteur de calcul résout les noms et les références inter-feuilles dans le processus
V := Book.Calculate('SUM(Data!D2:D100)*TaxRate');
if VarIsNumeric(V) then
  Log('net total checks out: ' + FloatToStr(V));

Calculate évalue une expression arbitraire sur l'état courant du classeur sans rien enregistrer, ce qui en fait la primitive d'assertion naturelle pour les tests de générateur. Calculez l'agrégat attendu à partir des données sources en Pascal, évaluez la formule du classeur lui-même, et comparez les deux. L'article sur le moteur de formules détaille ce que le moteur évalue, à quel moment, et comment l'étendre avec des fonctions personnalisées

Les noms _xlnm qui appartiennent à la couche des propriétés

Ouvrez la table des noms d'un fichier généré dans un inspecteur bas niveau et vous y trouverez des entrées que vous n'avez jamais écrites : _xlnm.Print_Area, _xlnm.Print_Titles et leurs proches. C'est ainsi que OOXML (ECMA-376 / ISO 29500) range les zones d'impression et les lignes de titre répétées, sous forme de noms définis à identifiants réservés. HotXLS les gère par des propriétés de feuille de calcul dédiées, si bien que renseigner PrintArea ou PrintTitleRows écrit l'entrée _xlnm.* correspondante à votre place

Le piège consiste à entrer à la main dans cet espace de noms réservé. Ajoutez une entrée _xlnm.Print_Area par DefinedNames.Add tout en renseignant aussi la propriété PrintArea et le classeur porte deux définitions contradictoires pour un même nom réservé, un état qu'Excel résout d'une manière sur laquelle aucun produit ne devrait compter. Considérez tout identifiant qui commence par _xlnm. comme appartenant à la couche des propriétés. Pour inspecter la configuration d'impression, lisez les propriétés, pas la table des noms. L'article sur la protection et la mise en page traite les propriétés de zone d'impression dans leur contexte

Deux limites à connaître avant d'arrêter une conception

Les noms définis ne suivent pas à travers la passerelle de commodité XLS vers XLSX. SaveXLSWorkbookAsXLSX copie le contenu des cellules et la mise en forme de base, et la table des noms ne figure pas sur sa liste de copie documentée, si bien qu'un classeur qui dépendait de ses noms les perd à la traversée. Recréez les noms par DefinedNames.Add après la conversion. Cette étape est moins pénible qu'elle en a l'air, car elle vous donne un moment pour normaliser leurs portées au lieu de reprendre ce que le fichier XLS contenait par hasard

L'autre limite est la dérive entre les chaînes de formules et les noms de feuilles. Excel réécrit les références de feuille dans les formules et les noms lors d'un renommage interactif, si bien que les fichiers modifiés par un utilisateur dans Excel restent cohérents tout seuls. L'exposition est du côté du générateur : quand du code Pascal assemble des chaînes de formules à partir d'un littéral de nom de feuille, renommer la feuille à un endroit et l'oublier à l'autre produit une référence vers une feuille qui n'existe plus. Gardez le nom de feuille dans une seule constante Delphi et alimentez-en à la fois Sheets.Add et votre assemblage de formules, et les deux ne pourront jamais diverger. C'est le même instinct qui plaide pour nommer les cellules de sortie d'un rapport plutôt que de coder les adresses en dur : un modèle dont la cellule de total est nommée continue de fonctionner après qu'un concepteur a inséré trois lignes au-dessus, tandis qu'un générateur qui écrit dans un B17 littéral pose discrètement son nombre au mauvais endroit. L'article sur la génération de rapports par modèle bâtit exactement sur ce motif

L'API complète des noms définis pour les deux formats, avec la référence du moteur de formules, est livrée avec le HotXLS Delphi Component