Copiez une plage depuis une grille Delphi et collez-la dans Word, et la mise en forme disparaît généralement : texte brut, pas d'en-têtes en gras, pas de bordures, pas de remplissages. HotXLS comble cet écart avec TXLSRange.CopyToClipboard, qui place une charge utile de presse-papiers CF_HTML — le format Windows pour du HTML stylé avec des marqueurs de fragment exacts à l'octet près — sur le presse-papiers à côté du texte Unicode brut
Cela paraît simple jusqu'à ce qu'on examine ce qu'exige réellement une charge utile CF_HTML. Le format a besoin d'un court en-tête texte nommant exactement où le fragment commence et se termine à l'intérieur du tampon de presse-papiers plus large, et ces positions sont des décalages d'octets, comptés à travers quel que soit l'encodage multi-octets dans lequel le HTML finit. Trompez-vous d'un seul octet dans l'arithmétique et l'application cible saisit soit la mauvaise tranche de balisage, soit abandonne et retombe sur du texte brut, et aucun de ces deux échecs ne ressemble à un bogue dans votre code — cela ressemble à Word étant Word
Pourquoi le copier-coller depuis une grille Delphi perd-il généralement sa mise en forme
L'appel presse-papiers Windows par défaut que la plupart du code Delphi utilise, SetClipboardData avec CF_TEXT ou CF_UNICODETEXT, ne transporte jamais que des caractères bruts, si bien que tout style appliqué dans la grille source n'a nulle part où aller. Word, Outlook, et tout navigateur basé sur Chromium recherchent un format plus riche lorsque vous collez : une représentation HTML de la sélection, complète avec styles en ligne, structure de tableau, et liens. Excel lui-même s'appuie exactement sur cette astuce — copiez une plage dans Excel et le presse-papiers reçoit silencieusement plusieurs formats à la fois, HTML parmi eux, si bien que quelle que soit l'application dans laquelle vous collez, elle choisit le plus riche qu'elle comprend. Un composant qui n'écrit jamais que CF_UNICODETEXT ne donne rien à travailler à chacun de ces consommateurs plus riches, et la richesse visuelle que l'utilisateur vient de copier n'est tout simplement pas là à coller
Qu'est-ce exactement que le format de presse-papiers CF_HTML ?
CF_HTML n'est pas un format de presse-papiers système fixe comme CF_TEXT ; c'est un format enregistré dynamiquement, demandé par nom via RegisterClipboardFormat('HTML Format'), et sa charge utile est un court en-tête ASCII suivi d'un document ou fragment HTML. L'en-tête porte cinq champs — Version, StartHTML, EndHTML, StartFragment, EndFragment — où Version vaut toujours 0.9 et les quatre autres sont des nombres décimaux écrits sous forme de chiffres ASCII. StartHTML et EndHTML délimitent le document entier tel que l'application réceptrice devrait l'analyser pour le contexte, polices et styles inclus, tandis que StartFragment et EndFragment délimitent la tranche plus étroite qui atterrit réellement au niveau du curseur, conventionnellement marquée dans le balisage lui-même par des commentaires <!--StartFragment--> et <!--EndFragment--> afin que les limites survivent à une re-sérialisation naïve
Décalages d'octets, pas comptages de caractères : le piège classique de CF_HTML
Les quatre champs numériques de l'en-tête CF_HTML sont des décalages d'octets dans la séquence exacte d'octets assise sur le presse-papiers, comptés depuis le tout premier caractère de l'en-tête lui-même — pas des comptages de caractères, pas des points de code Unicode, et pas des décalages relatifs au fragment ou à la balise <body>. Cette distinction est là où les implémentations CF_HTML écrites à la main se trompent silencieusement : la propriété Length d'un UnicodeString Delphi rapporte des unités de code UTF-16, ce qui se trouve égaler le nombre d'octets pour du texte ASCII pur, si bien que le bogue passe intact à travers tout test écrit avec des données d'exemple en anglais et n'apparaît qu'une fois qu'une cellule copiée contient un tiret cadratin, un symbole monétaire, ou un caractère accentué — un symbole euro est une unité de code UTF-16 mais trois octets en UTF-8, et chaque décalage calculé après ce point dérive du nombre d'octets supplémentaires que l'encodage a ajoutés. L'échec qui s'ensuit n'est pas un plantage ; c'est l'application réceptrice saisissant exactement la plage d'octets vers laquelle l'en-tête pointait, trouvant une tranche de balisage qui commence ou se termine en plein milieu d'une balise, et soit affichant du charabia, soit abandonnant et retombant sur le texte brut assis à côté sur le presse-papiers, silencieusement, sans rien dans votre code pour expliquer pourquoi — voici la forme de code qui produit exactement cet échec :
// Fragile: Length() on a UnicodeString counts UTF-16 code units, not bytes
var
Header: string;
Fragment: string;
StartFragmentOfs: Integer;
begin
Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
// A currency symbol, an em dash, or any accented character placed
// before this point costs one character here but two or three bytes
// once the document is UTF-8 encoded, so StartFragmentOfs now points
// short of where the fragment actually begins on the real clipboard
end;
Comment HotXLS garde l'en-tête précis à l'octet près
HotXLS évite structurellement cette classe de bogue : TXLSRange.CopyToClipboard et l'unité lxClipboard en dessous construisent le document CF_HTML et son en-tête entièrement en AnsiString, le type chaîne d'octets de Delphi, si bien que Length et Pos renvoient déjà des positions d'octets partout dans le calcul — il n'y a pas d'étape séparée, et donc pas d'étape à oublier, où un comptage de caractères Unicode devrait être converti en comptage d'octets avant d'entrer dans l'en-tête
Il existe une seconde astuce, plus petite, qui mérite d'être connue si vous construisez un jour un en-tête CF_HTML à la main. L'en-tête est écrit deux fois : une fois avec dix chiffres zéro tenant lieu de chacun des quatre décalages, afin que sa propre longueur en octets puisse être mesurée, et une seconde fois avec les vrais décalages substitués. Comme chaque décalage réel est formaté selon cette même largeur fixe de dix chiffres, le second en-tête ressort exactement de la même longueur en octets que la version placeholder, ce qui est précisément pourquoi la mesure antérieure reste valide après la réécriture. Sautez la largeur fixe, formatez un nombre avec un simple IntToStr à la place, et l'en-tête peut rétrécir ou grandir d'un chiffre entre les deux passes, invalidant silencieusement chaque décalage qui le suit :
const
Placeholder = '0000000000'; // 10 ASCII digits: fixed width in, fixed width out
var
Header: AnsiString; // AnsiString.Length is a byte count, not a char count
StartHtmlOfs: Integer;
begin
Header := 'Version:0.9'#13#10 +
'StartHTML:' + Placeholder + #13#10 +
'EndHTML:' + Placeholder + #13#10 +
'StartFragment:' + Placeholder + #13#10 +
'EndFragment:' + Placeholder + #13#10;
StartHtmlOfs := Length(Header); // safe to measure once, up front
// ...compute the real offsets against the AnsiString document...
// then rebuild Header with the real numbers formatted to the same
// 10-digit width, so its byte length -- and therefore StartHtmlOfs --
// never moves between the placeholder pass and the final one
end;
Pourquoi la charge utile en texte brut doit-elle tout de même accompagner l'ensemble
TXLSRange.CopyToClipboard ne place jamais CF_HTML seul sur le presse-papiers ; il écrit toujours CF_UNICODETEXT dans le même appel, car CF_HTML est un format enregistré plutôt que l'une des constantes fixes CF_* que toute application Windows sait déjà rechercher — un simple éditeur de texte, une grille héritée, ou quoi que ce soit qui n'a jamais vérifié la présence de 'HTML Format' ne le verra pas du tout, et la plage que vous avez copiée arrive soit sous forme de texte délimité par tabulations, soit n'arrive pas du tout. Ce texte délimité par tabulations n'est pas non plus une approximation grossière : les cellules de formule se copient sous forme de leur chaîne de formule avec un = initial restauré si le texte stocké l'avait supprimé, correspondant au comportement du propre texte de presse-papiers d'Excel, les cellules ordinaires copient leur FormattedText — la chaîne telle qu'affichée, si bien qu'une cellule monétaire se copie sous la forme 1 234,56 $, pas la valeur sous-jacente 1234.56 — et tout champ contenant une tabulation, une apostrophe, ou un saut de ligne est mis entre guillemets avec les guillemets intégrés doublés, la même convention qu'utilise le CSV
SaveAsHTML n'est pas un chemin de rendu séparé boulonné juste pour le cas du presse-papiers. CopyToClipboard appelle exactement le même écrivain HTML décrit dans l'export CSV, TSV et HTML de HotXLS, puis enveloppe ce que produit cet écrivain dans l'enveloppe CF_HTML au lieu de l'enregistrer comme un fichier autonome, si bien que tout ce qui est vrai de ce HTML se transmet directement à ce qui atterrit sur le presse-papiers. Rassembler une plage de feuille de calcul dans les deux formats en un seul appel ressemble à ceci :
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('quarterly-report.xlsx');
// Classic TXLSWorkbook ranges expose the identical method as
// Workbook.Sheets[1].Range['A1', 'F40'].CopyToClipboard
if Book.Sheets[1].Range['A1:F40'].CopyToClipboard then
ShowMessage('Range copied - press Ctrl+V in Word or a browser')
else
ShowMessage('Clipboard was busy; see the retry pattern below');
finally
Book.Free;
end;
end;
La plage collée conserve-t-elle ses polices, couleurs, et cellules fusionnées ?
Oui, car la moitié HTML de la charge utile est un rendu complet de la plage, pas un simple dépotoir de données : polices, couleurs de remplissage, bordures, formats de nombre, et cellules fusionnées passent toutes sous forme de styles en ligne et de structure de tableau, la même mécanique de style couverte dans le guide de HotXLS sur la mise en forme conditionnelle et le texte enrichi, puisque les segments de texte enrichi d'une cellule et le résultat de la mise en forme conditionnelle alimentent tous deux le même rendu dont CopyToClipboard tire ses informations. Ce qui ne survit pas au voyage, c'est le comportement de formule vivante : la forme en texte brut d'une cellule de formule porte la chaîne de formule, si bien qu'une cible de collage consciente des feuilles de calcul pourrait en principe la recalculer, mais la forme HTML ne porte jamais que le dernier résultat calculé, car HTML n'a aucune notion de formule qu'un navigateur ou un traitement de texte puisse évaluer
Vérifier le collage, et gérer un presse-papiers occupé
Deux habitudes attrapent la plupart des problèmes de presse-papiers avant qu'un client ne le fasse. Collez d'abord dans le Bloc-notes pour confirmer que le repli CF_UNICODETEXT est du texte délimité par tabulations sain, puis collez la même copie dans Word ou un navigateur pour confirmer que la version stylée apparaît — une charge utile qui a l'air correcte dans l'un et fausse dans l'autre signifie généralement que les marqueurs de fragment ont atterri au mauvais endroit. Traitez ensuite le résultat booléen que renvoie CopyToClipboard comme significatif, pas décoratif : OpenClipboard peut échouer lorsqu'un autre processus détient le presse-papiers ouvert, assez courant sur un bureau chargé pour qu'un appel non vérifié finisse par ne rien coller sans erreur pour expliquer pourquoi, ce contre quoi la nouvelle tentative ci-dessous protège :
function TryCopyRangeToClipboard(Workbook: TXLSXWorkbook): Boolean;
var
Attempt: Integer;
begin
Result := False;
for Attempt := 1 to 5 do
begin
Result := Workbook.Sheets[1].Range['A1:F40'].CopyToClipboard;
if Result then
Break;
Sleep(50); // give whichever app is holding the clipboard a moment
end;
if not Result then
raise Exception.Create('Could not take ownership of the clipboard');
end;
Le format lui-même n'a rien d'exotique une fois que l'en-tête est précis à l'octet près et que le repli en texte brut est honnête sur ce qu'il contient — il existe pratiquement inchangé depuis qu'Internet Explorer l'a défini pour la première fois, et chaque application Windows majeure continue de le lire de la même façon. CopyToClipboard se trouve aux côtés de PasteFromClipboard, le côté lecture du même échange, dans la surface de presse-papiers et d'export plus large documentée sur la page produit du composant HotXLS