Kopieer een bereik uit een Delphi-grid en plak het in Word, en de opmaak verdwijnt meestal: platte tekst, geen vetgedrukte kopregels, geen randen, geen vullingen. HotXLS overbrugt dat gat met TXLSRange.CopyToClipboard, dat een CF_HTML-klembordpayload — het Windows-formaat voor opgemaakte HTML met byte-exacte fragmentmarkeringen — naast platte Unicode-tekst op het klembord plaatst
Dat klinkt eenvoudig totdat u kijkt naar wat een CF_HTML-payload werkelijk vereist. Het formaat heeft een korte tekstheader nodig die precies aangeeft waar het fragment begint en eindigt binnen de grotere klembordbuffer, en die posities zijn byte-offsets, geteld door welke multi-byte-codering de HTML ook uiteindelijk krijgt. Reken de wiskunde ook maar één byte fout en de doeltoepassing pakt hetzij het verkeerde stukje markup, hetzij geeft het op en valt terug op platte tekst, en geen van beide storingen ziet eruit als een bug in uw code — het ziet eruit alsof Word gewoon Word is
Waarom kopiëren-plakken vanuit een Delphi-grid meestal de opmaak verliest
De standaard Windows-klembordaanroep die de meeste Delphi-code gebruikt, SetClipboardData met CF_TEXT of CF_UNICODETEXT, draagt alleen ooit platte tekens, dus elke opmaak die op het bron-grid is toegepast heeft nergens naartoe. Word, Outlook en elke Chromium-gebaseerde browser zoeken bij het plakken naar een rijker formaat: een HTML-representatie van de selectie, compleet met inline stijlen, tabelstructuur en links. Excel zelf leunt precies op deze truc — kopieer een bereik in Excel en het klembord ontvangt stilletjes meerdere formaten tegelijk, HTML daaronder, zodat welke toepassing u ook plakt het rijkste formaat kiest dat het begrijpt. Een component die alleen ooit CF_UNICODETEXT schrijft, geeft al die rijkere consumenten niets om mee te werken, en de visuele rijkdom die de gebruiker net kopieerde is er simpelweg niet om te plakken
Wat is het CF_HTML-klembordformaat precies?
CF_HTML is geen vast systeemklembordformaat zoals CF_TEXT; het is een dynamisch geregistreerd formaat, opgevraagd bij naam via RegisterClipboardFormat('HTML Format'), en de payload ervan is een korte ASCII-header gevolgd door een HTML-document of -fragment. De header draagt vijf velden — Version, StartHTML, EndHTML, StartFragment, EndFragment — waarbij Version altijd 0.9 is en de andere vier decimale getallen zijn, uitgeschreven als ASCII-cijfers. StartHTML en EndHTML begrenzen het hele document zoals de ontvangende toepassing het voor context moet parsen, inclusief lettertypen en stijlen, terwijl StartFragment en EndFragment het smallere stuk begrenzen dat daadwerkelijk bij de cursor terechtkomt, conventioneel gemarkeerd in de markup zelf met de commentaarregels <!--StartFragment--> en <!--EndFragment-->, zodat de grenzen een naïeve heropbouw van de serialisatie overleven
Byte-offsets, geen tekentellingen: de klassieke CF_HTML-valstrik
De vier numerieke headervelden van CF_HTML zijn byte-offsets in de exacte bytereeks die op het klembord staat, geteld vanaf het allereerste teken van de header zelf — geen tekentellingen, geen Unicode-codepunten, en geen offsets relatief aan het fragment of de <body>-tag. Dat onderscheid is precies waar handgeschreven CF_HTML-implementaties stilletjes de mist ingaan: de Length van een Delphi UnicodeString geeft UTF-16-code-eenheden terug, wat toevallig gelijk is aan het aantal bytes voor platte ASCII-tekst, dus de bug glipt schoon door elke test die met Engelse voorbeeldgegevens is geschreven en komt pas aan het licht zodra een gekopieerde cel een liggend streepje, een valutasymbool of een teken met accent bevat — een euroteken is één UTF-16-code-eenheid maar drie bytes in UTF-8, en elke offset die na dat punt wordt berekend, verschuift met zoveel extra bytes als de codering heeft toegevoegd. De storing die daarop volgt is geen crash; het is de ontvangende toepassing die precies het byte-bereik grijpt waar de header naar wees, daar een stukje markup aantreft dat halverwege een tag begint of eindigt, en dan ofwel rommel weergeeft ofwel het opgeeft en terugvalt op wat voor platte tekst er ook naast staat op het klembord, stilletjes, zonder dat er iets in uw code staat om te verklaren waarom — hieronder staat de vorm van code die precies die storing veroorzaakt:
// 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;
Hoe HotXLS de header byte-nauwkeurig houdt
HotXLS vermijdt deze klasse bugs structureel: TXLSRange.CopyToClipboard en de eronderliggende unit lxClipboard bouwen het CF_HTML-document en de header ervan volledig op als AnsiString, Delphi's bytestring-type, zodat Length en Pos overal in de berekening al byteposities teruggeven — er is geen aparte stap, en dus geen stap om te vergeten, waarin een Unicode-tekentelling naar een bytetelling zou moeten worden omgezet voordat deze in de header terechtkomt
Er is een tweede, kleinere truc die de moeite waard is om te kennen als u ooit zelf een CF_HTML-header opbouwt. De header wordt tweemaal geschreven: eenmaal met tien nulcijfers als plaatsvervanger voor elk van de vier offsets, zodat de eigen bytelengte gemeten kan worden, en nogmaals met de echte offsets ingevuld. Omdat elke echte offset in diezelfde vaste breedte van tien cijfers wordt geformatteerd, komt de tweede header byte-voor-byte even lang uit als de plaatsvervangerversie, en precies daarom blijft de eerdere meting geldig na de herschrijving. Sla de vaste breedte over, formatteer een getal in plaats daarvan met een gewone IntToStr, en de header kan tussen de twee passages een cijfer krimpen of groeien, waardoor elke offset die erna komt stilletjes ongeldig wordt:
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;
Waarom de platte-tekstpayload toch moet meereizen
TXLSRange.CopyToClipboard plaatst nooit CF_HTML alleen op het klembord; het schrijft in dezelfde aanroep altijd ook CF_UNICODETEXT, omdat CF_HTML een geregistreerd formaat is in plaats van een van de vaste CF_*-constanten die elke Windows-toepassing al weet te zoeken — een platte teksteditor, een verouderd grid, of iets dat nooit heeft gecontroleerd op 'HTML Format' zal het helemaal niet zien, en het bereik dat u kopieerde komt ofwel binnen als tab-gescheiden tekst, ofwel komt het niet binnen. Die tab-gescheiden tekst is ook geen ruwe benadering: formulecellen kopiëren als hun formulestring met een voorloop-= hersteld als de opgeslagen tekst deze liet vallen, wat overeenkomt met hoe Excel's eigen klembordtekst zich gedraagt, gewone cellen kopiëren hun FormattedText — de string zoals weergegeven, dus een valutacel kopieert als $1.234,56, niet de onderliggende 1234.56 — en elk veld dat een tab, een aanhalingsteken of een regeleinde bevat wordt tussen aanhalingstekens gezet met verdubbelde ingesloten aanhalingstekens, dezelfde conventie die CSV gebruikt
SaveAsHTML is geen apart renderpad dat alleen voor het klembordgeval is aangeplakt. CopyToClipboard roept dezelfde HTML-writer aan die wordt beschreven in HotXLS's CSV-, TSV- en HTML-export, en wikkelt vervolgens wat die writer produceert in de CF_HTML-envelop in plaats van het als op zichzelf staand bestand op te slaan, dus alles wat waar is over die HTML komt rechtstreeks door in wat op het klembord terechtkomt. Een werkbladbereik in één aanroep als beide formaten samenbrengen ziet er zo uit:
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;
Behoudt het geplakte bereik zijn lettertypen, kleuren en samengevoegde cellen?
Ja, omdat de HTML-helft van de payload een volledige weergave van het bereik is, geen kale datadump: lettertypen, vulkleuren, randen, getalnotaties en samengevoegde cellen komen allemaal door als inline stijlen en tabelstructuur, dezelfde opmaakmachinerie die wordt behandeld in HotXLS's gids over voorwaardelijke opmaak en opgemaakte tekst, aangezien zowel de rich-text-runs van een cel als het resultaat van voorwaardelijke opmaak dezelfde rendering voeden waaruit CopyToClipboard leest. Wat de reis niet overleeft is levend formuliergedrag: de platte-tekstvorm van een formulecel draagt de formulestring, dus een spreadsheet-bewust plakdoel zou deze in principe opnieuw kunnen berekenen, maar de HTML-vorm draagt alleen ooit het laatst berekende resultaat, omdat HTML geen begrip heeft van een formule die een browser of tekstverwerker kan evalueren
Het plakken verifiëren, en een bezet klembord afhandelen
Twee gewoontes vangen de meeste klembordproblemen op voordat een klant dat doet. Plak eerst in Kladblok om te bevestigen dat de CF_UNICODETEXT-terugval verstandige tab-gescheiden tekst is, plak vervolgens dezelfde kopie in Word of een browser om te bevestigen dat de opgemaakte versie verschijnt — een payload die er in de ene goed uitziet en in de andere fout, betekent meestal dat de fragmentmarkeringen op de verkeerde plek terechtkwamen. Behandel vervolgens het Booleaanse resultaat dat CopyToClipboard teruggeeft als betekenisvol, niet decoratief: OpenClipboard kan mislukken wanneer een ander proces het klembord open houdt, wat vaak genoeg voorkomt op een druk bezet bureaublad dat één ongecontroleerde aanroep uiteindelijk niets plakt zonder foutmelding om te verklaren waarom — precies waartegen de retry hieronder beschermt:
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;
Het formaat zelf is niet exotisch zodra de header byte-nauwkeurig is en de platte-tekst-terugval eerlijk is over wat deze bevat — het bestaat grotendeels ongewijzigd sinds Internet Explorer het voor het eerst definieerde, en elke grote Windows-toepassing leest het nog steeds op dezelfde manier. CopyToClipboard staat naast PasteFromClipboard, de leeskant van dezelfde uitwisseling, in het bredere klembord- en exportoppervlak dat wordt gedocumenteerd op de productpagina van de HotXLS-component