Kopiér et interval fra et Delphi-grid, og indsæt det i Word, og formateringen forsvinder som regel: ren tekst, ingen fede overskrifter, ingen kanter, ingen udfyldninger. HotXLS lukker det hul med TXLSRange.CopyToClipboard, som lægger en CF_HTML-udklipsholder-payload — Windows-formatet til stylet HTML med byte-nøjagtige fragment-markører — på udklipsholderen ved siden af almindelig Unicode-tekst
Det lyder simpelt, indtil man ser på, hvad en CF_HTML-payload rent faktisk kræver. Formatet har brug for en kort tekst-header, der navngiver præcis, hvor fragmentet begynder og slutter inde i den større udklipsholder-buffer, og de positioner er byte-forskydninger, talt gennem hvilken som helst multi-byte-encoding, HTML'en ender med. Får man aritmetikken forkert med bare én byte, griber målapplikationen enten den forkerte skive markup eller giver op og falder tilbage til ren tekst, og ingen af de fejl ligner en bug i ens kode — den ligner Word der er Word
Hvorfor kopier-indsæt fra et Delphi-grid som regel mister sin formatering
Det standard-Windows-udklipsholder-kald, det meste Delphi-kode griber til, SetClipboardData med CF_TEXT eller CF_UNICODETEXT, bærer kun nogensinde rene tegn, så enhver stilisering anvendt i kilde-gridet har ingen steder at gå hen. Word, Outlook og enhver Chromium-baseret browser leder efter et rigere format, når man indsætter: en HTML-repræsentation af markeringen, komplet med inline-stile, tabelstruktur og links. Excel selv er afhængig af netop det trick — kopiér et interval i Excel, og udklipsholderen modtager i stilhed flere formater på én gang, HTML iblandt dem, så uanset hvilken applikation man indsætter i, vælger den rigeste, den forstår. En komponent, der kun nogensinde skriver CF_UNICODETEXT, overdrager hver af de rigere forbrugere intet at arbejde med, og den visuelle rigdom brugeren lige kopierede, er simpelthen ikke der at indsætte
Hvad er CF_HTML-udklipsholderformatet præcis?
CF_HTML er ikke et fast systemudklipsholderformat som CF_TEXT; det er et dynamisk registreret et, anmodet om ved navn gennem RegisterClipboardFormat('HTML Format'), og dets payload er en kort ASCII-header efterfulgt af et HTML-dokument eller -fragment. Headeren bærer fem felter — Version, StartHTML, EndHTML, StartFragment, EndFragment — hvor Version altid er 0.9, og de andre fire er decimaltal skrevet ud som ASCII-cifre. StartHTML og EndHTML afgrænser hele dokumentet, som den modtagende applikation bør parse det for kontekst, skrifttyper og stile inkluderet, mens StartFragment og EndFragment afgrænser den smallere skive, der rent faktisk lander ved markøren, konventionelt markeret i selve markup'en med <!--StartFragment-->- og <!--EndFragment-->-kommentarer, så grænserne overlever naiv genserialisering
Byte-forskydninger, ikke tegnoptællinger: den klassiske CF_HTML-fælde
CF_HTML's fire numeriske header-felter er byte-forskydninger ind i den præcise byte-sekvens, der sidder på udklipsholderen, talt fra selve headerens allerførste tegn — ikke tegnoptællinger, ikke Unicode-kodepunkter, og ikke forskydninger relativt til fragmentet eller <body>-tagget. Det skel er, hvor håndrullede CF_HTML-implementeringer i stilhed går galt: en Delphi UnicodeString's Length rapporterer UTF-16-kodeenheder, hvilket tilfældigvis er lig byteantallet for ren ASCII-tekst, så bugen slipper ren gennem enhver test skrevet med engelske eksempeldata og viser sig først, når en kopieret celle indeholder en em-streg, et valutasymbol eller et accenteret tegn — et euro-tegn er én UTF-16-kodeenhed, men tre bytes i UTF-8, og hver forskydning beregnet efter det punkt driver med, hvor mange ekstra bytes encodingen tilføjede. Den fejl, der følger, er ikke et crash; det er den modtagende applikation, der griber det præcise byte-interval, headeren pegede på, finder en skive markup, der starter eller slutter midt i et tag, og enten gengiver skrammel eller giver op og falder tilbage til hvilken som helst ren tekst, der sidder ved siden af den på udklipsholderen, i stilhed, uden noget i ens kode til at forklare hvorfor — her er formen af kode, der producerer netop den fejl:
// 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;
Hvordan HotXLS holder headeren byte-nøjagtig
HotXLS undgår denne klasse af bug strukturelt: TXLSRange.CopyToClipboard og lxClipboard-unit'en under den bygger CF_HTML-dokumentet og dets header helt som AnsiString, Delphis byte-streng-type, så Length og Pos allerede returnerer byte-positioner overalt i beregningen — der er intet separat trin, og derfor intet trin at glemme, hvor en Unicode-tegnoptælling ville have brug for at blive konverteret til en byte-optælling, før den går ind i headeren
Der er endnu et, mindre trick værd at kende, hvis man nogensinde bygger en CF_HTML-header i hånden. Headeren skrives to gange: én gang med ti nul-cifre som pladsholder for hver af de fire forskydninger, så dens egen bytelængde kan måles, og én gang mere med de rigtige forskydninger indsat. Fordi hver rigtig forskydning formateres til den samme faste ti-cifrede bredde, kommer den anden header ud med byte-for-byte samme længde som pladsholder-versionen, hvilket er præcis, hvorfor den tidligere måling forbliver gyldig efter omskrivningen. Spring den faste bredde over, formater et tal med en almindelig IntToStr i stedet, og headeren kan krympe eller vokse med et ciffer mellem de to gennemløb, og i stilhed gøre hver forskydning, der følger den, ugyldig:
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;
Hvorfor ren-tekst-payloaden stadig skal følge med
TXLSRange.CopyToClipboard lægger aldrig CF_HTML på udklipsholderen alene; den skriver altid CF_UNICODETEXT i det samme kald, fordi CF_HTML er et registreret format frem for en af de faste CF_*-konstanter, enhver Windows-applikation allerede ved at lede efter — en ren tekstredigering, et legacy-grid, eller noget som helst der aldrig tjekkede for 'HTML Format', vil slet ikke se det, og det interval man kopierede, ankommer enten som tab-afgrænset tekst, eller det gør ikke. Den tab-afgrænsede tekst er heller ikke en grov tilnærmelse: formelceller kopierer som deres formel-streng med et indledende = genoprettet, hvis den lagrede tekst droppede det, matchende hvordan Excels egen udklipsholder-tekst opfører sig, almindelige celler kopierer deres FormattedText — strengen som vist, så en valutacelle kopierer som $1.234,56, ikke den underliggende 1234.56 — og ethvert felt, der indeholder en tab, et anførselstegn eller et linjeskift, bliver citeret med indlejrede anførselstegn fordoblet, den samme konvention CSV bruger
SaveAsHTML er ikke en separat gengivelsesvej boltet på bare til udklipsholder-tilfældet. CopyToClipboard kalder den samme HTML-skriver beskrevet i HotXLS' CSV-, TSV- og HTML-eksport, og pakker derefter, hvad end den skriver producerer, ind i CF_HTML-kuverten i stedet for at gemme det som en selvstændig fil, så alt, hvad der er sandt om den HTML, bæres direkte over i, hvad der lander på udklipsholderen. At samle et regnearksinterval som begge formater i ét kald ser sådan ud:
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;
Beholder det indsatte interval sine skrifttyper, farver og sammenlagte celler?
Ja, fordi HTML-halvdelen af payloaden er en fuld gengivelse af intervallet, ikke et rent datadump: skrifttyper, fyldfarver, kanter, taleformater og sammenlagte celler kommer alle igennem som inline-stile og tabelstruktur, det samme stiliserings-maskineri dækket i HotXLS' guide til betinget formatering og rig tekst, da en celles rige-tekst-forløb og betinget-formaterings-resultat begge fodrer den samme gengivelse, CopyToClipboard læser fra. Hvad der ikke overlever turen, er levende-formel-opførsel: en formelcelles rene-tekst-form bærer formel-strengen, så et regnearks-bevidst indsætningsmål i princippet kunne genberegne den, men HTML-formen bærer kun nogensinde det sidst beregnede resultat, fordi HTML ikke har noget koncept om en formel, en browser eller en tekstbehandler kan evaluere
Verificering af indsætningen, og håndtering af en optaget udklipsholder
To vaner fanger de fleste udklipsholder-problemer, før en kunde gør. Indsæt først i Notesblok for at bekræfte, at CF_UNICODETEXT-fallbacken er fornuftig tab-afgrænset tekst, indsæt derefter den samme kopi i Word eller en browser for at bekræfte, at den stylede version dukker op — en payload der ser rigtig ud i den ene og forkert i den anden, betyder som regel, at fragment-markørerne landede det forkerte sted. Behandl derefter den boolske returværdi CopyToClipboard giver som meningsfuld, ikke dekorativ: OpenClipboard kan fejle, når en anden proces holder udklipsholderen åben, almindeligt nok på et travlt skrivebord til at ét utjekket kald til sidst indsætter ingenting uden nogen fejl til at forklare hvorfor, hvilket er det, genforsøget nedenfor beskytter mod:
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;
Selve formatet er ikke eksotisk, når først headeren er byte-nøjagtig, og ren-tekst-fallbacken er ærlig om, hvad den indeholder — det har eksisteret stort set uændret siden Internet Explorer først definerede det, og enhver stor Windows-applikation læser det stadig på samme måde. CopyToClipboard sidder ved siden af PasteFromClipboard, læse-siden af den samme udveksling, i den bredere udklipsholder- og eksport-flade dokumenteret på HotXLS-komponentens produktside