Skopírujte rozsah z gridu v Delphi a vložte ho do Wordu, a formátovanie zvyčajne zmizne: obyčajný text, žiadne tučné hlavičky, žiadne orámovania, žiadne výplne. HotXLS túto medzeru uzatvára pomocou TXLSRange.CopyToClipboard, ktorá vloží na schránku payload CF_HTML — formát Windows pre štýlované HTML s bajtovo presnými značkovačmi fragmentu — vedľa obyčajného unicode textu
To znie jednoducho, kým sa nepozriete na to, čo payload CF_HTML v skutočnosti vyžaduje. Formát potrebuje krátku textovú hlavičku pomenúvajúcu presne to, kde fragment vnútri väčšieho bufferu schránky začína a končí, a tieto pozície sú bajtové posuny, počítané cez akékoľvek viacbajtové kódovanie, v akom HTML nakoniec skončí. Pomýlite aritmetiku čo i len o jeden bajt a cieľová aplikácia buď zachytí nesprávny výrez značkovania, alebo to vzdá a spadne späť na obyčajný text, a ani jedno z týchto zlyhaní nevyzerá ako chyba vo vašom kóde — vyzerá to, akoby Word bol proste Word
Prečo kopírovanie-vkladanie z gridu v Delphi zvyčajne stratí formátovanie
Predvolené volanie schránky Windows, po ktorom väčšina kódu v Delphi siaha, SetClipboardData s CF_TEXT alebo CF_UNICODETEXT, nesie iba obyčajné znaky, takže akékoľvek štýlovanie aplikované v zdrojovom gride nemá kam ísť. Word, Outlook a každý prehliadač založený na Chromiu pri vkladaní hľadajú bohatší formát: HTML reprezentáciu výberu, kompletnú s inline štýlmi, štruktúrou tabuľky a odkazmi. Samotný Excel spolieha presne na tento trik — skopírujte rozsah v Exceli a schránka ticho dostane niekoľko formátov naraz, HTML medzi nimi, takže ktorákoľvek aplikácia, do ktorej vkladáte, si vyberie ten najbohatší, ktorému rozumie. Komponent, ktorý zapisuje iba CF_UNICODETEXT, dá každému z týchto bohatších konzumentov nič na prácu, a vizuálna bohatosť, ktorú používateľ práve skopíroval, jednoducho nie je k dispozícii na vloženie
Čo presne je formát schránky CF_HTML?
CF_HTML nie je pevný systémový formát schránky ako CF_TEXT; je to dynamicky registrovaný formát, vyžiadaný podľa mena cez RegisterClipboardFormat('HTML Format'), a jeho payload je krátka hlavička ASCII nasledovaná HTML dokumentom alebo fragmentom. Hlavička nesie päť polí — Version, StartHTML, EndHTML, StartFragment, EndFragment — kde Version je vždy 0.9 a ostatné štyri sú desiatkové čísla zapísané ako ASCII číslice. StartHTML a EndHTML ohraničujú celý dokument tak, ako by ho mala prijímajúca aplikácia parsovať kvôli kontextu, vrátane písiem a štýlov, zatiaľ čo StartFragment a EndFragment ohraničujú užší výrez, ktorý sa skutočne ocitne na kurzore, konvenčne označený priamo v značkovaní komentármi <!--StartFragment--> a <!--EndFragment-->, aby hranice prežili naivnú opätovnú serializáciu
Bajtové posuny, nie počty znakov: klasická pasca CF_HTML
Štyri číselné polia hlavičky CF_HTML sú bajtové posuny do presnej postupnosti bajtov sediacej na schránke, počítané od úplne prvého znaku samotnej hlavičky — nie počty znakov, nie unicode body kódu, a nie posuny relatívne voči fragmentu alebo tagu <body>. Práve v tomto rozlíšení sa ručne písané implementácie CF_HTML ticho pomýlia: Length reťazca UnicodeString v Delphi hlási jednotky kódu UTF-16, čo sa náhodou rovná počtu bajtov pre obyčajný ASCII text, takže chyba prejde čisto cez akýkoľvek test napísaný s anglickými vzorovými dátami a objaví sa až vtedy, keď skopírovaná bunka obsahuje pomlčku em, symbol meny, alebo znak s diakritikou — znak eura je jedna jednotka kódu UTF-16, no tri bajty v UTF-8, a každý posun vypočítaný po tomto bode sa posunie o toľko dodatočných bajtov, koľko kódovanie pridalo. Zlyhanie, ktoré nasleduje, nie je pád; je to prijímajúca aplikácia, ktorá zachytí presne ten bajtový rozsah, na ktorý hlavička ukázala, nájde výrez značkovania, ktorý začína alebo končí uprostred tagu, a buď vykreslí nezmysel, alebo to vzdá a spadne späť na akýkoľvek obyčajný text, ktorý pri ňom sedí na schránke, potichu, bez čohokoľvek vo vašom kóde, čo by vysvetlilo prečo — tu je tvar kódu, ktorý produkuje presne takéto zlyhanie:
// 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;
Ako HotXLS udržuje hlavičku bajtovo presnú
HotXLS sa tejto triede chýb vyhýba štrukturálne: TXLSRange.CopyToClipboard a jednotka lxClipboard pod ňou zostavujú dokument CF_HTML a jeho hlavičku úplne ako AnsiString, bajtový reťazcový typ Delphi, takže Length a Pos už vracajú bajtové pozície všade vo výpočte — neexistuje samostatný krok, a teda ani žiadny krok na zabudnutie, kde by bolo treba počet unicode znakov previesť na počet bajtov ešte pred tým, než sa dostane do hlavičky
Existuje druhý, menší trik, ktorý sa oplatí poznať, ak niekedy staviate hlavičku CF_HTML ručne. Hlavička sa zapíše dvakrát: raz s desiatimi nulovými číslicami zastupujúcimi každý zo štyroch posunov, aby sa dala zmerať jej vlastná dĺžka v bajtoch, a znova s doplnenými skutočnými posunmi. Keďže každý skutočný posun je formátovaný na tú istú pevnú šírku desiatich číslic, druhá hlavička vyjde bajt po bajte rovnako dlhá ako verzia s náhradnými hodnotami, čo je presne dôvod, prečo skoršie meranie zostane platné aj po prepísaní. Preskočte pevnú šírku, naformátujte číslo obyčajným IntToStr namiesto toho, a hlavička sa môže medzi oboma priechodmi zmenšiť alebo zväčšiť o jednu číslicu, čím potichu zneplatní každý posun, ktorý po nej nasleduje:
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;
Prečo sa payload obyčajného textu stále musí viezť spolu
TXLSRange.CopyToClipboard nikdy neumiestni CF_HTML na schránku samotné; vždy v tom istom volaní zapíše aj CF_UNICODETEXT, pretože CF_HTML je registrovaný formát, nie jedna z pevných konštánt CF_*, ktoré každá aplikácia Windows už vie hľadať — obyčajný textový editor, starší grid, alebo čokoľvek, čo nikdy nekontrolovalo 'HTML Format', ho vôbec neuvidí, a rozsah, ktorý ste skopírovali, buď dorazí ako text oddelený tabulátormi, alebo nedorazí vôbec. Ten text oddelený tabulátormi nie je ani hrubá aproximácia: bunky so vzorcom sa kopírujú ako reťazec svojho vzorca s obnoveným úvodným =, ak ho uložený text stratil, čo zodpovedá tomu, ako sa správa vlastný text schránky Excelu, obyčajné bunky kopírujú svoj FormattedText — reťazec tak, ako je zobrazený, takže bunka s menou sa skopíruje ako $1 234,56, nie podkladová hodnota 1234.56 — a akékoľvek pole obsahujúce tabulátor, úvodzovku, alebo zalomenie riadka sa uzatvorí do úvodzoviek so zdvojenými vloženými úvodzovkami, rovnaká konvencia, akú používa CSV
SaveAsHTML nie je samostatná vykresľovacia cesta priskrutkovaná len pre prípad schránky. CopyToClipboard volá presne toho istého zapisovača HTML opísaného v exporte CSV, TSV a HTML v HotXLS, a potom čokoľvek, čo tento zapisovač vyprodukuje, obalí do obálky CF_HTML namiesto toho, aby to uložil ako samostatný súbor, takže čokoľvek platí pre toto HTML, sa prenáša priamo do toho, čo skončí na schránke. Zhrnutie rozsahu hárka ako oboch formátov v jednom volaní vyzerá takto:
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;
Zachová si vložený rozsah svoje písma, farby a zlúčené bunky?
Áno, pretože polovica payloadu s HTML je plné vykreslenie rozsahu, nie holý výpis dát: písma, farby výplne, orámovania, formáty čísel a zlúčené bunky, to všetko prechádza ako inline štýly a štruktúra tabuľky, tá istá štýlovacia mašinéria opísaná v sprievodcovi HotXLS pre podmienené formátovanie a formátovaný text, keďže behy formátovaného textu bunky aj výsledok podmieneného formátovania napájajú to isté vykreslenie, z ktorého CopyToClipboard číta. Čo cestu neprežije, je živé správanie vzorca: obyčajná textová forma bunky so vzorcom nesie reťazec vzorca, takže cieľ vkladania rozumejúci tabuľkám by ho v princípe mohol znova prepočítať, no forma HTML vždy nesie iba posledný vypočítaný výsledok, pretože HTML nemá žiadny koncept vzorca, ktorý by prehliadač alebo textový procesor mohol vyhodnotiť
Overenie vloženia a zvládanie zaneprázdnenej schránky
Dva zvyky odhalia väčšinu problémov so schránkou skôr, než ich odhalí zákazník. Najprv vložte do Poznámkového bloku, aby ste potvrdili, že náhrada CF_UNICODETEXT je rozumný text oddelený tabulátormi, potom vložte tú istú kópiu do Wordu alebo prehliadača, aby ste potvrdili, že sa objaví štýlovaná verzia — payload, ktorý vyzerá správne v jednom a zle v druhom, zvyčajne znamená, že značkovače fragmentu skončili na nesprávnom mieste. Potom berte booleovský výsledok, ktorý CopyToClipboard vracia, ako zmysluplný, nie ozdobný: OpenClipboard môže zlyhať, keď iný proces drží schránku otvorenú, dosť bežné na zaneprázdnenom desktope, že jedno nekontrolované volanie nakoniec nevloží nič bez chyby, ktorá by vysvetlila prečo, čo je presne to, pred čím chráni nižšie uvedené opakovanie:
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;
Samotný formát nie je exotický vo chvíli, keď je hlavička bajtovo presná a náhrada obyčajného textu je čestná o tom, čo obsahuje — existuje z veľkej časti nezmenený od chvíle, keď ho prvýkrát definoval Internet Explorer, a každá väčšia aplikácia Windows ho stále číta rovnako. CopyToClipboard sedí vedľa PasteFromClipboard, čítacej strany tej istej výmeny, na širšej ploche schránky a exportu zdokumentovanej na stránke produktu komponentu HotXLS