Copiați un interval dintr-un grid Delphi și lipiți-l în Word, iar formatarea de obicei dispare: text simplu, fără antete îngroșate, fără borduri, fără umpleri. HotXLS închide acel gol cu TXLSRange.CopyToClipboard, care pune o încărcătură de clipboard CF_HTML — formatul Windows pentru HTML stilizat cu marcatori de fragment exacți la nivel de octet — pe clipboard, alături de text Unicode simplu
Asta sună simplu până priviți ce cere de fapt o încărcătură CF_HTML. Formatul are nevoie de un antet text scurt care numește exact unde începe și se termină fragmentul în interiorul bufferului de clipboard mai mare, iar acele poziții sunt offset-uri de octeți, numărate prin orice codificare multi-octet ajunge să aibă HTML-ul. Greșiți aritmetica cu chiar un singur octet, iar aplicația țintă fie apucă felia greșită de markup, fie renunță și revine la text simplu, și niciun eșec nu arată ca un bug în codul dvs. — arată ca Word fiind Word
De ce copy-paste dintr-un grid Delphi de obicei pierde formatarea
Apelul implicit de clipboard Windows la care apelează majoritatea codului Delphi, SetClipboardData cu CF_TEXT sau CF_UNICODETEXT, poartă întotdeauna doar caractere simple, așa că orice stilizare aplicată în grid-ul sursă nu are unde să meargă. Word, Outlook și fiecare browser bazat pe Chromium caută un format mai bogat atunci când lipiți: o reprezentare HTML a selecției, completă cu stiluri inline, structură de tabel și link-uri. Excel însuși se bazează exact pe acest truc — copiați un interval în Excel, iar clipboard-ul primește silențios mai multe formate deodată, HTML printre ele, astfel încât orice aplicație lipiți alege pe cel mai bogat pe care îl înțelege. O componentă care scrie mereu doar CF_UNICODETEXT predă fiecăruia din acei consumatori mai bogați nimic cu care să lucreze, iar bogăția vizuală pe care utilizatorul tocmai a copiat-o pur și simplu nu este acolo pentru a fi lipită
Ce este exact formatul de clipboard CF_HTML?
CF_HTML nu este un format de clipboard fix de sistem precum CF_TEXT; este unul înregistrat dinamic, cerut după nume prin RegisterClipboardFormat('HTML Format'), iar încărcătura sa este un antet ASCII scurt urmat de un document sau fragment HTML. Antetul poartă cinci câmpuri — Version, StartHTML, EndHTML, StartFragment, EndFragment — unde Version este întotdeauna 0.9, iar celelalte patru sunt numere zecimale scrise ca cifre ASCII. StartHTML și EndHTML delimitează întregul document așa cum ar trebui aplicația destinatară să îl analizeze pentru context, fonturi și stiluri incluse, în timp ce StartFragment și EndFragment delimitează felia mai restrânsă care efectiv aterizează la cursor, marcată convențional în markup-ul însuși cu comentarii <!--StartFragment--> și <!--EndFragment-->, astfel încât limitele supraviețuiesc unei reserializări naive
Offset-uri de octeți, nu numărători de caractere: capcana clasică CF_HTML
Cele patru câmpuri numerice de antet ale CF_HTML sunt offset-uri de octeți în secvența exactă de octeți aflată pe clipboard, numărate de la chiar primul caracter al antetului însuși — nu numărători de caractere, nu puncte de cod Unicode, și nu offset-uri relative la fragment sau la eticheta <body>. Acea distincție este unde implementările CF_HTML scrise manual eșuează silențios: un UnicodeString Delphi raportează unități de cod UTF-16 la Length, ceea ce se întâmplă să fie egal cu numărul de octeți pentru text ASCII simplu, așa că bug-ul trece curat prin orice test scris cu date de exemplu în engleză și apare abia odată ce o celulă copiată conține o linie em, un simbol monetar, sau un caracter accentuat — un simbol euro este o unitate de cod UTF-16, dar trei octeți în UTF-8, iar fiecare offset calculat după acel punct deviază cu oricâți octeți suplimentari a adăugat codificarea. Eșecul care urmează nu este un crash; este aplicația destinatară sesizând exact intervalul de octeți indicat de antet, găsind o felie de markup care începe sau se termină la mijlocul unei etichete, și fie randând gunoi, fie renunțând și revenind la orice text simplu stă alături de el pe clipboard, silențios, fără nimic în codul dvs. care să explice de ce — iată forma de cod care produce exact acel eșec:
// 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;
Cum menține HotXLS antetul exact la nivel de octet
HotXLS evită această clasă de bug structural: TXLSRange.CopyToClipboard și unitatea lxClipboard de dedesubt construiesc documentul CF_HTML și antetul său în întregime ca AnsiString, tipul de șir de octeți al Delphi, astfel încât Length și Pos returnează deja poziții de octeți peste tot în calcul — nu există niciun pas separat, și deci niciun pas de uitat, unde o numărătoare de caractere Unicode ar trebui convertită într-o numărătoare de octeți înainte de a intra în antet
Există un al doilea truc, mai mic, care merită cunoscut dacă vreodată construiți manual un antet CF_HTML. Antetul este scris de două ori: o dată cu zece cifre zero care țin locul fiecăruia din cele patru offset-uri, astfel încât propria sa lungime de octeți poate fi măsurată, și încă o dată cu offset-urile reale corectate. Pentru că fiecare offset real este formatat la aceeași lățime fixă de zece cifre, al doilea antet iese cu exact aceeași lungime, octet-cu-octet, ca versiunea placeholder, ceea ce este exact motivul pentru care măsurarea anterioară rămâne validă după rescriere. Săriți peste lățimea fixă, formatați un număr cu un simplu IntToStr în schimb, iar antetul poate să se micșoreze sau să crească cu o cifră între cele două treceri, invalidând silențios fiecare offset care îl urmează:
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;
De ce trebuie să meargă în continuare alături încărcătura de text simplu
TXLSRange.CopyToClipboard nu pune niciodată CF_HTML pe clipboard singur; scrie întotdeauna CF_UNICODETEXT în același apel, pentru că CF_HTML este un format înregistrat, nu unul din constantele fixe CF_* pe care fiecare aplicație Windows deja știe să le caute — un editor de text simplu, un grid moștenit, sau orice nu a verificat vreodată 'HTML Format' nu îl va vedea deloc, iar intervalul pe care l-ați copiat fie sosește ca text delimitat prin tab, fie nu sosește deloc. Acel text delimitat prin tab nu este nici măcar o aproximare aproximativă: celulele cu formulă se copiază ca șirul lor de formulă cu un = inițial restaurat dacă textul stocat l-a eliminat, potrivindu-se cu modul în care se comportă propriul text de clipboard al Excel, celulele obișnuite copiază FormattedText-ul lor — șirul așa cum este afișat, așa că o celulă de monedă se copiază ca $1,234.56, nu valoarea de bază 1234.56 — și orice câmp care conține un tab, un ghilimel, sau o întrerupere de linie este pus între ghilimele cu ghilimelele încorporate dublate, aceeași convenție folosită de CSV
SaveAsHTML nu este o cale de randare separată adăugată doar pentru cazul clipboard-ului. CopyToClipboard apelează exact același scriitor HTML descris în exportul CSV, TSV și HTML al HotXLS, apoi învelește orice produce acel scriitor în plicul CF_HTML, în loc să îl salveze ca un fișier independent, așa că orice este adevărat despre acel HTML se transferă direct în ce ajunge pe clipboard. Adunarea unui interval de foaie de calcul ca ambele formate într-un singur apel arată așa:
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;
Păstrează intervalul lipit fonturile, culorile și celulele îmbinate?
Da, pentru că jumătatea HTML a încărcăturii este o randare completă a intervalului, nu un simplu vărsare de date: fonturile, culorile de umplere, bordurile, formatele numerice și celulele îmbinate ajung toate ca stiluri inline și structură de tabel, aceeași mecanică de stilizare acoperită în ghidul HotXLS despre formatarea condiționată și textul îmbogățit, întrucât atât secvențele de text îmbogățit ale unei celule, cât și rezultatul formatării condiționate alimentează aceeași randare pe care o citește CopyToClipboard. Ce nu supraviețuiește călătoriei este comportamentul de formulă vie: forma text simplu a unei celule cu formulă poartă șirul formulei, așa că o țintă de lipire conștientă de foi de calcul ar putea în principiu s-o recalculeze, dar forma HTML poartă întotdeauna doar ultimul rezultat calculat, pentru că HTML nu are niciun concept de formulă pe care un browser sau un procesor de text să o evalueze
Verificarea lipirii și gestionarea unui clipboard ocupat
Două obiceiuri prind majoritatea problemelor de clipboard înainte ca un client să o facă. Lipiți mai întâi în Notepad pentru a confirma că rezerva CF_UNICODETEXT este text delimitat prin tab sănătos, apoi lipiți aceeași copie în Word sau un browser pentru a confirma că versiunea stilizată apare — o încărcătură care arată corect într-unul și greșit în celălalt de obicei înseamnă că marcatorii de fragment au aterizat în locul greșit. Apoi tratați rezultatul Boolean pe care îl returnează CopyToClipboard ca semnificativ, nu decorativ: OpenClipboard poate eșua când un alt proces ține clipboard-ul deschis, suficient de comun pe un desktop ocupat încât un apel neverificat ajunge eventual să lipească nimic fără nicio eroare care să explice de ce, ceea ce este exact ce previne reîncercarea de mai jos:
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;
Formatul însuși nu este exotic odată ce antetul este exact la nivel de octet, iar rezerva de text simplu este onestă despre ce conține — a existat în mare parte neschimbat de când Internet Explorer l-a definit pentru prima dată, iar fiecare aplicație majoră Windows încă îl citește la fel. CopyToClipboard stă alături de PasteFromClipboard, partea de citire a aceluiași schimb, în suprafața mai largă de clipboard și export documentată pe pagina de produs componenta HotXLS