Kada kopirate raspon iz Delphi mreže i zalijepite ga u Word, oblikovanje obično nestane: ostaje običan tekst, bez podebljanih zaglavlja, obruba i ispuna. HotXLS taj jaz zatvara metodom TXLSRange.CopyToClipboard, koja na međuspremnik uz običan Unicode tekst smješta i CF_HTML sadržaj — Windowsov format za stilizirani HTML s oznakama fragmenta i pomacima bajtova određenima do bajta
To zvuči jednostavno dok ne pogledate što CF_HTML sadržaj zapravo zahtijeva. Format treba kratko tekstualno zaglavlje koje točno navodi gdje fragment počinje i završava unutar većeg međuspremničkog spremnika, a ti su položaji pomaci u bajtovima izračunati kroz kodiranje s više bajtova u kojem HTML na kraju završi. Ako aritmetiku pogriješite za samo jedan bajt, ciljna aplikacija uzme pogrešan dio oznaka ili odustane i vrati se na običan tekst, a nijedan kvar ne izgleda kao pogreška u vašem kodu — izgleda kao da Word jednostavno radi po svom
Zašto kopiranje iz Delphi mreže obično gubi oblikovanje
Zadani poziv Windowsova međuspremnika za kojim poseže većina Delphi koda, SetClipboardData s CF_TEXT ili CF_UNICODETEXT, prenosi samo obične znakove, pa oblikovanje primijenjeno u izvornoj mreži nema kamo otići. Word, Outlook i svaki preglednik temeljen na Chromiumu pri lijepljenju traže bogatiji format: HTML prikaz odabira s ugrađenim stilovima, strukturom tablice i poveznicama. Sam Excel oslanja se upravo na taj trik — kada kopirate raspon u Excelu, međuspremnik potajno primi više formata odjednom, među njima i HTML, pa aplikacija u koju lijepite odabere najbogatiji format koji razumije. Komponenta koja ikada zapisuje samo CF_UNICODETEXT svim tim bogatijim potrošačima ne daje ništa s čime bi mogli raditi, pa vizualno bogatstvo koje je korisnik upravo kopirao jednostavno ne postoji za lijepljenje
Što je točno format međuspremnika CF_HTML
CF_HTML nije nepromjenjivi sistemski format međuspremnika poput CF_TEXT; riječ je o dinamički registriranom formatu koji se po nazivu zatraži putem RegisterClipboardFormat('HTML Format'), a njegov sadržaj čine kratko ASCII zaglavlje i HTML dokument ili fragment. Zaglavlje nosi pet polja — Version, StartHTML, EndHTML, StartFragment i EndFragment — pri čemu je Version uvijek 0.9, a ostala četiri polja decimalni brojevi zapisani ASCII znamenkama. StartHTML i EndHTML omeđuju cijeli dokument koji primajuća aplikacija treba analizirati radi konteksta, uključujući fontove i stilove, dok StartFragment i EndFragment omeđuju uži dio koji stvarno završava na pokazivaču, a granice se uobičajeno označavaju u samim oznakama komentarima <!--StartFragment--> i <!--EndFragment--> kako bi preživjele naivnu ponovnu serijalizaciju
Pomaci bajtova, a ne broj znakova: klasična zamka CF_HTML
Četiri brojčana polja zaglavlja CF_HTML-a jesu pomaci u bajtovima unutar točnog niza bajtova koji se nalazi na međuspremniku, a brojanje počinje od prvog znaka samog zaglavlja — to nisu brojevi znakova, Unicode kodnih točaka ni pomaci relativni prema fragmentu ili oznaci <body>. Upravo tu ručno pisane implementacije CF_HTML-a potajno griješe: Length Delphi UnicodeString vrijednosti vraća broj UTF-16 kodnih jedinica, što je jednako broju bajtova za običan ASCII tekst, pa greška bez problema prođe svaki test s engleskim primjerima i pokaže se tek kada kopirana ćelija sadrži dugu crticu, simbol valute ili naglašeni znak — znak eura zauzima jednu UTF-16 kodnu jedinicu, ali tri bajta u UTF-8, pa svaki pomak izračunat nakon njega odstupa za broj dodatnih bajtova koje je kodiranje dodalo. Posljedica nije rušenje programa, nego primajuća aplikacija uzme točan raspon bajtova na koji pokazuje zaglavlje, pronađe dio oznaka koji počinje ili završava usred oznake te prikaže smeće ili se tiho vrati na običan tekst koji stoji uz HTML na međuspremniku, bez ičega u vašem kodu što bi objasnilo zašto — ovako izgleda kod koji stvara upravo taj kvar:
// 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;
Kako HotXLS održava točnost pomaka u bajtovima zaglavlja
HotXLS ovu klasu pogrešaka izbjegava strukturno: TXLSRange.CopyToClipboard i jedinica lxClipboard koja se nalazi ispod nje grade CF_HTML dokument i zaglavlje u cijelosti kao AnsiString, Delphijev tip niza bajtova, pa Length i Pos svugdje u izračunu već vraćaju položaje u bajtovima — nema zasebnog koraka, pa nema ni koraka koji bi se mogao zaboraviti, u kojem bi broj Unicode znakova trebalo pretvoriti u broj bajtova prije nego što uđe u zaglavlje
Postoji još jedan manji trik koji vrijedi znati ako ikada ručno gradite CF_HTML zaglavlje. Zaglavlje se zapisuje dvaput: prvi put s deset nula kao mjestima za svaki od četiri pomaka, kako bi se mogla izmjeriti njegova vlastita duljina u bajtovima, a drugi put sa stvarnim pomacima umetnutima na njihova mjesta. Budući da se svaki stvarni pomak formatira u isti fiksni niz od deset znamenki, drugo zaglavlje ima potpuno istu duljinu u bajtovima kao verzija s rezerviranim mjestima, zbog čega ranije mjerenje ostaje valjano nakon ponovnog zapisa. Ako preskočite fiksnu širinu i umjesto toga broj formatirate običnim IntToStr, zaglavlje se između ta dva prolaza može skratiti ili produljiti za jednu znamenku, pa se svaki sljedeći pomak tiho poništava:
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;
Zašto uz HTML sadržaj mora ići i običan tekst
TXLSRange.CopyToClipboard nikada ne stavlja samo CF_HTML na međuspremnik; u istom pozivu uvijek zapisuje i CF_UNICODETEXT, jer je CF_HTML registrirani format, a ne jedna od fiksnih konstanti CF_* koje svaka Windowsova aplikacija već zna potražiti — uređivač običnog teksta, naslijeđena mreža ili bilo što što nikada nije provjerilo 'HTML Format' uopće ga neće vidjeti, pa će kopirani raspon stići kao tekst odvojen tabulatorima ili neće stići. Taj tekst odvojen tabulatorima nije ni gruba aproksimacija: ćelije s formulama kopiraju se kao niz formule s vraćenom početnom oznakom = ako je spremljeni tekst ostao bez nje, baš kao što se ponaša Excelov tekst međuspremnika, obične ćelije kopiraju svoj FormattedText — niz kakav je prikazan, pa se valutna ćelija kopira kao $1,234.56, a ne kao osnovnih 1234.56 — dok se svako polje koje sadrži tabulator, navodnik ili prijelom retka navodnicima štiti tako da se ugrađeni navodnici udvostruče, po istoj konvenciji koju koristi CSV
SaveAsHTML nije zaseban put iscrtavanja naknadno pričvršćen samo za slučaj međuspremnika. CopyToClipboard poziva isti HTML pisač opisan u članku CSV, TSV i HTML izvoz u HotXLS-u, a zatim sve što taj pisač proizvede omata u CF_HTML omotnicu umjesto da to spremi kao samostalnu datoteku, pa se sve što vrijedi za taj HTML izravno prenosi i na sadržaj međuspremnika. Dohvaćanje raspona radnog lista u oba formata jednim pozivom izgleda ovako:
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;
Zadržava li zalijepljeni raspon fontove, boje i spojene ćelije
Da, jer je HTML dio sadržaja potpuno iscrtavanje raspona, a ne goli izvoz podataka: fontovi, boje ispune, obrubi, formati brojeva i spojene ćelije prenose se kao ugrađeni stilovi i struktura tablice, istim mehanizmom stiliziranja koji je obrađen u vodiču za uvjetno oblikovanje i obogaćeni tekst u HotXLS-u, budući da i rasponi obogaćenog teksta ćelije i rezultat uvjetnog oblikovanja ulaze u isto iscrtavanje koje čita CopyToClipboard. Ono što ne preživi prijenos jest ponašanje aktivne formule: tekstualni oblik ćelije s formulom nosi niz formule, pa bi cilj za lijepljenje koji razumije proračunske tablice načelno mogao ponovno izračunati vrijednost, ali HTML oblik nosi samo posljednji izračunati rezultat, jer preglednik ili program za obradu teksta nema pojam formule koji bi mogao procijeniti
Provjera lijepljenja i rukovanje zauzetim međuspremnikom
Dvije navike otkriju većinu problema s međuspremnikom prije korisnika. Najprije zalijepite u Notepad kako biste potvrdili da je rezervni CF_UNICODETEXT smislen tekst razdvojen tabulatorima, a zatim istu kopiju zalijepite u Word ili preglednik kako biste potvrdili da se stilizirana verzija prikazuje — sadržaj koji izgleda ispravno na jednom mjestu, a pogrešno na drugom, obično znači da su oznake fragmenta završile na pogrešnom mjestu. Zatim rezultat tipa Boolean koji vraća CopyToClipboard tretirajte kao važan podatak, a ne ukras: OpenClipboard može ne uspjeti kada drugi proces drži međuspremnik otvorenim, što je na zauzetom računalu dovoljno uobičajeno da jedan neprovjeren poziv na kraju ne zalijepi ništa bez objašnjenja, a upravo to štiti sljedeći pokušaj:
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;
Sam format nije egzotičan kada je zaglavlje točno u bajtovima, a rezervni obični tekst iskreno opisuje što sadrži — uglavnom je nepromijenjen od trenutka kada ga je prvi definirao Internet Explorer, a svaka velika Windowsova aplikacija i dalje ga čita na isti način. CopyToClipboard stoji uz PasteFromClipboard, stranu za čitanje istog prijenosa, u širem skupu funkcija međuspremnika i izvoza opisanom na stranici proizvoda HotXLS komponenta