Másolj ki egy tartományt egy Delphi gridből, és illeszd be Wordbe, és a formázás általában eltűnik: sima szöveg, nincs félkövér fejléc, nincs szegély, nincs kitöltés. A HotXLS ezt a rést a TXLSRange.CopyToClipboard-dal zárja be, amely egy CF_HTML vágólap-payloadot — a stílusos HTML Windows-formátumát, bájtpontos fragmensjelölőkkel — helyez a vágólapra a sima Unicode-szöveg mellett
Ez egyszerűnek hangzik, amíg meg nem nézed, mit igényel valójában egy CF_HTML payload. A formátum egy rövid szöveges fejlécet igényel, amely pontosan megnevezi, hol kezdődik és végződik a fragmens a nagyobb vágólappufferen belül, és ezek a pozíciók bájteltolások, bármilyen többbájtos kódolással megszámolva, amiben az HTML végül van. Rontsd el az aritmetikát akár egyetlen bájttal, és a célalkalmazás vagy a rossz markup-szeletet kapja el, vagy feladja, és sima szövegre esik vissza, és egyik hiba sem néz ki hibának a kódodban — úgy néz ki, mintha a Word csak a Word lenne
Miért veszíti el a formázását a másolás-beillesztés egy Delphi gridből?
Az alapértelmezett Windows-vágólap-hívás, amelyhez a legtöbb Delphi-kód nyúl, a SetClipboardData CF_TEXT-tel vagy CF_UNICODETEXT-tel, csak valaha sima karaktereket hordoz, így bármely stílus, amelyet a forrás gridben alkalmaztak, nincs hova mennie. A Word, az Outlook, és minden Chromium-alapú böngésző egy gazdagabb formátumot keres, amikor beillesztesz: a kijelölés egy HTML-reprezentációját, teljes soros stílusokkal, táblaszerkezettel, és linkekkel. Maga az Excel pontosan erre a trükkre támaszkodik — másolj ki egy tartományt az Excelben, és a vágólap csendben egyszerre több formátumot kap, köztük a HTML-t, így bármelyik alkalmazásba illeszted be, az felveszi a legdúsabbat, amit ért. Egy komponens, amely csak valaha CF_UNICODETEXT-et ír, semmit nem ad a kezébe egyik ilyen gazdagabb fogyasztónak sem, és a vizuális gazdagság, amit a felhasználó épp kimásolt, egyszerűen nincs ott, amit beilleszthetne
Pontosan mi az a CF_HTML vágólapformátum?
A CF_HTML nem fix rendszer-vágólapformátum, mint a CF_TEXT; ez egy dinamikusan regisztrált formátum, névvel kérve a RegisterClipboardFormat('HTML Format')-on keresztül, és a payloadja egy rövid ASCII fejléc, amit egy HTML-dokumentum vagy fragmens követ. A fejléc öt mezőt hordoz — Version, StartHTML, EndHTML, StartFragment, EndFragment —, ahol a Version mindig 0.9, és a másik négy decimális szám, ASCII számjegyekként kiírva. A StartHTML és EndHTML határolja a teljes dokumentumot úgy, ahogyan a fogadó alkalmazásnak elemeznie kellene azt kontextusért, betűtípusokkal és stílusokkal együtt, míg a StartFragment és EndFragment határolja a szűkebb szeletet, amely ténylegesen a kurzornál landol, hagyományosan magában a markupban <!--StartFragment--> és <!--EndFragment--> megjegyzésekkel jelölve, így a határok túlélik a naiv újraszerializálást
Bájteltolások, nem karakterszámok: a klasszikus CF_HTML csapda
A CF_HTML négy numerikus fejlécmezője bájteltolás a vágólapon ülő bájtok pontos sorozatába, magától a fejléc első karakterétől számolva — nem karakterszám, nem Unicode-kódpontok, és nem a fragmenshez vagy a <body> taghez viszonyított eltolás. Ez a megkülönböztetés az, ahol a kézzel hengerelt CF_HTML-megvalósítások csendben elromlanak: egy Delphi UnicodeString Length-je UTF-16 kódegységeket jelent, ami véletlenül egyenlő a bájtszámmal sima ASCII-szövegnél, így a hiba tisztán átcsúszik bármely angol mintaadatokkal írt teszten, és csak akkor mutatkozik meg, amikor egy másolt cella egy gondolatjelet, egy pénznemszimbólumot, vagy egy ékezetes karaktert tartalmaz — egy euró jel egy UTF-16 kódegység, de három bájt UTF-8-ban, és minden azon pont után kiszámított eltolás annyi extra bájttal sodródik el, amennyit a kódolás hozzáadott. A következő hiba nem összeomlás; a fogadó alkalmazás megragadja pontosan azt a bájttartományt, amire a fejléc mutatott, egy olyan markup-szeletet talál, amely tag közepén kezdődik vagy végződik, és vagy szemetet renderel, vagy feladja, és arra a sima szövegre esik vissza, ami mellette ül a vágólapon, csendben, semmivel a kódodban, ami megmagyarázná, miért — íme az a kódalak, amely pontosan ezt a hibát állítja elő:
// 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;
Hogyan tartja a HotXLS bájtpontosan a fejlécet
A HotXLS strukturálisan kerüli el ezt a hibaosztályt: a TXLSRange.CopyToClipboard és az alatta lévő lxClipboard unit teljesen AnsiString-ként építi fel a CF_HTML dokumentumot és fejlécét, a Delphi bájt-sztring típusaként, így a Length és a Pos már mindenhol bájtpozíciókat ad vissza a számításban — nincs külön lépés, és ezért nincs lépés, amit elfelejtenénk, ahol egy Unicode karakterszámot bájtszámmá kellene alakítani, mielőtt a fejlécbe kerülne
Van egy második, kisebb trükk, amit érdemes tudni, ha valaha kézzel építesz egy CF_HTML fejlécet. A fejléc kétszer kerül megírásra: egyszer tíz nulla számjeggyel a négy eltolás mindegyike helyén, hogy saját bájthossza mérhető legyen, és egyszer még, a valódi eltolásokkal befoltozva. Mivel minden valódi eltolás ugyanarra a fix tízszámjegyű szélességre van formázva, a második fejléc bájtról bájtra ugyanolyan hosszúra jön ki, mint a helykitöltő verzió, ami pontosan azért van, hogy a korábbi mérés érvényes maradjon az újraírás után. Hagyd ki a fix szélességet, formázz egy számot egyszerű IntToStr-rel ehelyett, és a fejléc egy számjeggyel zsugorodhat vagy nőhet a két átfutás között, csendben érvénytelenítve minden azt követő eltolást:
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;
Miért kell a sima-szöveg payloadnak mégis mindig ott lovagolnia
A TXLSRange.CopyToClipboard soha nem helyezi a CF_HTML-t egyedül a vágólapra; mindig ír CF_UNICODETEXT-et ugyanabban a hívásban, mert a CF_HTML egy regisztrált formátum, nem a fix CF_* konstansok egyike, amit minden Windows-alkalmazás már tud keresni — egy egyszerű szövegszerkesztő, egy örökölt grid, vagy bármi, ami soha nem ellenőrizte a 'HTML Format'-ot, egyáltalán nem fogja látni azt, és a kimásolt tartomány vagy tab-elválasztott szövegként érkezik, vagy nem érkezik. Az a tab-elválasztott szöveg sem durva közelítés: a képletcellák a képletsztringjükként másolódnak vissza egy visszaállított kezdő =-vel, ha a tárolt szöveg elejtette azt, illeszkedve ahhoz, hogyan viselkedik maga az Excel vágólapszövege, a közönséges cellák a FormattedText-jüket másolják — a sztringet ahogy megjelenik, így egy pénznem-cella $1 234,56-ként másolódik, nem az alapul szolgáló 1234,56-ként —, és bármely mező, amely tabot, idézőjelet, vagy sortörést tartalmaz, idézőjelbe kerül, a beágyazott idézőjelekkel megduplázva, ugyanaz a konvenció, amit a CSV használ
A SaveAsHTML nem egy külön renderelési útvonal, ami csak a vágólap-esethez van rácsavarozva. A CopyToClipboard ugyanazt a HTML-írót hívja, amelyet a HotXLS CSV-, TSV-, és HTML-exportja ír le, majd bármit is állít elő az az író, becsomagolja a CF_HTML burokba ahelyett, hogy önálló fájlként mentené, így bármi, ami igaz arra a HTML-re, egyenesen átmegy abba, ami a vágólapon landol. Egy munkalap-tartomány egy hívásban mindkét formátumként való összehúzása így néz ki:
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;
Megtartja a beillesztett tartomány a betűtípusait, színeit, és összevont celláit?
Igen, mert a payload HTML-fele a tartomány teljes renderelése, nem puszta adatdömping: a betűtípusok, kitöltőszínek, szegélyek, számformátumok, és összevont cellák mind átjönnek soros stílusokként és táblaszerkezetként, ugyanaz a stílusgépezet, amit a HotXLS feltételes formázásról és formázott szövegről szóló útmutatója tárgyal, mivel egy cella formázottszöveg-futamai és feltételesformázás-eredménye egyaránt ugyanabba a renderelésbe táplálnak, amiből a CopyToClipboard olvas. Ami nem éli túl az utat, az az élő-képlet viselkedés: egy képletcella sima-szöveg formája hordozza a képletsztringet, így egy táblázat-tudatos beillesztési cél elméletileg újraszámolhatná, de a HTML-forma csak valaha az utolsó kiszámított eredményt hordozza, mert a HTML-nek nincs fogalma egy képletről, amit egy böngésző vagy egy szövegszerkesztő kiértékelhetne
A beillesztés ellenőrzése, és egy foglalt vágólap kezelése
Két szokás fogja el a legtöbb vágólapproblémát, mielőtt egy ügyfél megtenné. Illeszd be először a Jegyzettömbbe, hogy megerősítsd, a CF_UNICODETEXT-visszaesés épeszű tab-elválasztott szöveg, majd illeszd be ugyanazt a másolatot Wordbe vagy egy böngészőbe, hogy megerősítsd, a stílusos verzió megjelenik — egy payload, amely az egyikben helyesnek, a másikban rossznak néz ki, általában azt jelenti, hogy a fragmensjelölők rossz helyre landoltak. Aztán kezeld a logikai eredményt, amit a CopyToClipboard ad vissza, jelentőségteljesként, ne díszítésként: az OpenClipboard elbukhat, amikor egy másik folyamat nyitva tartja a vágólapot, ami elég gyakori egy foglalt asztali gépen ahhoz, hogy egy ellenőrizetlen hívás végül semmit ne illesszen be semmilyen hibával, ami megmagyarázná, miért, és ez ellen véd az alábbi újrapróbálkozás:
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;
Maga a formátum nem egzotikus, amint a fejléc bájtpontos, és a sima-szöveg-visszaesés őszinte abban, mit tartalmaz — nagyrészt változatlanul létezik, mióta az Internet Explorer először definiálta, és minden nagyobb Windows-alkalmazás még mindig ugyanúgy olvassa. A CopyToClipboard a PasteFromClipboard mellett ül, ugyanannak a cserének az olvasási oldalán, a szélesebb vágólap- és exportfelületen, amelyet a HotXLS Komponens termékoldala dokumentál