Nukopijuokite diapazoną iš Delphi tinklelio ir įklijuokite jį į Word — formatavimas paprastai dingsta: lieka tik paprastas tekstas, nėra paryškintų antraščių, rėmelių ar užpildų. HotXLS pašalina šią spragą naudodamas TXLSRange.CopyToClipboard, kuris šalia paprasto Unicode teksto į iškarpinę įrašo CF_HTML turinį — Windows formatą stilizuotam HTML su tiksliais fragmento baitų žymekliais
Tai skamba paprastai, kol nepažiūrite, ko iš tikrųjų reikia CF_HTML turiniui. Formatui reikia trumpos tekstinės antraštės, tiksliai nurodančios, kur fragmentas prasideda ir baigiasi didesniame iškarpinės buferyje, o šios pozicijos yra baitų poslinkiai, skaičiuojami pagal daugiabaitį kodavimą, kuriuo galiausiai pateikiamas HTML. Suklydus skaičiavime vos vienu baitu, tikslinė programa paima neteisingą žymėjimo dalį arba pasiduoda ir grįžta prie paprasto teksto, o nė vienas gedimas neatrodo kaip jūsų kodo klaida — atrodo, kad Word tiesiog elgiasi kaip Word
Kodėl kopijuojant iš Delphi tinklelio formatavimas paprastai prarandamas
Numatytasis Windows iškarpinės iškvietimas, kurį renkasi dauguma Delphi kodo, SetClipboardData su CF_TEXT arba CF_UNICODETEXT, perduoda tik paprastus simbolius, todėl šaltinio tinkleliui pritaikytas stilius neturi kur patekti. Word, Outlook ir kiekviena Chromium pagrindu veikianti naršyklė įklijuojant ieško turtingesnio formato: pasirinkimo HTML atvaizdo su įterptais stiliais, lentelės struktūra ir nuorodomis. Pats Excel remiasi lygiai tokiu triuku — nukopijavus diapazoną programoje Excel, iškarpinė tyliai gauna kelis formatus, įskaitant HTML, todėl programa, į kurią įklijuojate, pasirenka turtingiausią suprantamą formatą. Komponentas, įrašantis tik CF_UNICODETEXT, nepalieka nieko, ką galėtų naudoti turtingesni vartotojai, todėl ką tik nukopijuotas vaizdinis turinys nebegali būti įklijuotas
Kas tiksliai yra CF_HTML iškarpinės formatas
CF_HTML nėra fiksuotas sistemos iškarpinės formatas, kaip CF_TEXT; tai dinamiškai registruojamas formatas, užklausiama jo pavadinimu per RegisterClipboardFormat('HTML Format'), o jo turinį sudaro trumpa ASCII antraštė, po kurios eina HTML dokumentas arba fragmentas. Antraštėje yra penki laukai — Version, StartHTML, EndHTML, StartFragment, EndFragment — kuriuose Version visada yra 0.9, o kiti keturi yra dešimtainiai skaičiai, užrašyti ASCII skaitmenimis. StartHTML ir EndHTML apibrėžia visą dokumentą, kurį gaunanti programa turi analizuoti kontekstui, šriftams ir stiliams, o StartFragment ir EndFragment apibrėžia siauresnę dalį, kuri iš tikrųjų įkeliama ties žymekliu; ji įprastai pačiame žymėjime pažymima komentarais <!--StartFragment--> ir <!--EndFragment-->, kad ribos išliktų net ir naiviai iš naujo serializuojant
Baitų poslinkiai, o ne simbolių skaičius: klasikinė CF_HTML klaida
Keturi skaitiniai CF_HTML antraštės laukai yra baitų poslinkiai į tikslią iškarpinėje esančią baitų seką, skaičiuojami nuo paties pirmojo antraštės simbolio — tai nėra simbolių skaičius, Unicode kodo taškai ar poslinkiai nuo fragmento arba <body> žymos. Būtent čia rankiniu būdu kuriamos CF_HTML realizacijos tyliai sugenda: Delphi UnicodeString tipo Length grąžina UTF-16 kodo vienetus, kurie paprastam ASCII tekstui sutampa su baitų skaičiumi, todėl klaida nepastebimai praeina bet kokį testą su angliškais pavyzdiniais duomenimis ir pasirodo tik tada, kai nukopijuotame langelyje atsiranda ilgas brūkšnys, valiutos simbolis arba kirčiuotas simbolis — euro ženklas yra vienas UTF-16 kodo vienetas, bet UTF-8 koduotėje užima tris baitus, todėl kiekvienas po šio taško apskaičiuotas poslinkis pasislenka tiek, kiek papildomų baitų pridėjo kodavimas. Tai nėra strigtis; gaunanti programa paima tikslų antraštėje nurodytą baitų intervalą, randa žymėjimo dalį, prasidedančią arba pasibaigiančią žymos viduryje, ir arba atvaizduoja šiukšles, arba tyliai pasiduoda ir grįžta prie šalia iškarpinėje esančio paprasto teksto, o jūsų kode nėra nieko, kas paaiškintų kodėl — štai tokį gedimą sukuriančio kodo forma:
// 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;
Kaip HotXLS išlaiko baitų tikslumo antraštę
HotXLS struktūriškai išvengia šios klaidos klasės: TXLSRange.CopyToClipboard ir po juo esantis lxClipboard modulis CF_HTML dokumentą bei jo antraštę kuria tik kaip AnsiString, Delphi baitų eilutės tipą, todėl Length ir Pos visur skaičiavimuose jau grąžina baitų pozicijas — nereikia atskiro veiksmo, kurį būtų galima pamiršti, konvertuojant Unicode simbolių skaičių į baitų skaičių prieš įrašant jį į antraštę
Jei kada nors CF_HTML antraštę kursite rankiniu būdu, verta žinoti dar vieną mažesnį triuką. Antraštė įrašoma du kartus: pirmą kartą kiekvienam iš keturių poslinkių naudojama dešimt nulio skaitmenų, kad būtų galima išmatuoti jos pačios baitų ilgį, o antrą kartą įrašomi tikrieji poslinkiai. Kadangi kiekvienas tikras poslinkis formatuojamas naudojant tą patį fiksuotą dešimties skaitmenų plotį, antroji antraštė baitas į baitą yra tokio pat ilgio kaip vietaženklio versija, todėl ankstesnis matavimas po perrašymo lieka galiojantis. Praleidus fiksuotą plotį ir vietoj jo naudojant paprastą IntToStr, skaičius tarp dviejų etapų gali sutrumpinti arba pailginti antraštę vienu skaitmeniu ir tyliai padaryti negaliojančiais visus po jos esančius poslinkius:
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;
Kodėl kartu būtinas ir paprasto teksto turinys
TXLSRange.CopyToClipboard niekada neįrašo į iškarpinę vien CF_HTML; tuo pačiu iškvietimu jis visada įrašo ir CF_UNICODETEXT, nes CF_HTML yra registruotas formatas, o ne vienas iš fiksuotų CF_* konstantų, kurių kiekviena Windows programa jau moka ieškoti — paprasto teksto redaktorius, senesnis tinklelis ar bet kuri programa, niekada netikrinusi 'HTML Format', jo visai nepamatys, todėl nukopijuotas diapazonas arba bus gautas kaip tekstas su tabuliacijos ženklais, arba nebus gautas. Be to, toks tekstas su tabuliacijos ženklais nėra grubus apytikslis variantas: formulės langeliai kopijuojami kaip formulės eilutė, o jei išsaugotame tekste pradinis = buvo pašalintas, jis atkuriamas taip, kaip veikia paties Excel iškarpinės tekstas; įprasti langeliai kopijuoja savo FormattedText — rodomą eilutę, todėl valiutos langelis kopijuojamas kaip $1,234.56, o ne kaip vidinė reikšmė 1234.56; o kiekvienas laukas, kuriame yra tabuliacijos ženklas, kabutė arba eilutės lūžis, apskliaudžiamas kabutėmis, o vidinės kabutės padvigubinamos pagal tą pačią taisyklę, kurią naudoja CSV
SaveAsHTML nėra atskiras atvaizdavimo kelias, prijungtas vien iškarpinės atvejui. CopyToClipboard iškviečia tą patį HTML rašytuvą, aprašytą HotXLS CSV, TSV ir HTML eksporto vadove, tada apgaubia jo sukurtą turinį CF_HTML apvalkalu, užuot išsaugojęs jį kaip atskirą failą, todėl viskas, kas galioja tam HTML, tiesiogiai patenka ir į iškarpinę. Darbalapio diapazoną abiem formatais vienu iškvietimu galima gauti taip:
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;
Ar įklijuotas diapazonas išlaiko šriftus, spalvas ir sulietus langelius
Taip, nes HTML turinio dalis yra visas diapazono atvaizdas, o ne paprastas duomenų išrašas: šriftai, užpildų spalvos, rėmeliai, skaičių formatai ir sulieti langeliai perduodami kaip įterpti stiliai bei lentelės struktūra, naudojant tą pačią stilių apdorojimo sistemą, kuri aprašyta HotXLS sąlyginio formatavimo ir raiškiojo teksto vadove, nes langelio raiškiojo teksto dalys ir sąlyginio formatavimo rezultatas patenka į tą patį atvaizdą, iš kurio skaito CopyToClipboard. Neperduodamas tik gyvų formulių veikimas: paprasto teksto formoje formulės langelyje yra formulės eilutė, todėl skaičiuoklę suprantanti įklijavimo paskirties programa teoriškai galėtų ją perskaičiuoti, tačiau HTML formoje visada perduodamas tik paskutinis apskaičiuotas rezultatas, nes HTML naršyklėms ar tekstų rengyklėms neturi formulės, kurią jos galėtų įvertinti, sąvokos
Įklijavimo tikrinimas ir užimtos iškarpinės tvarkymas
Du įpročiai padeda aptikti daugumą iškarpinės problemų anksčiau, nei jas pastebi klientas. Pirmiausia įklijuokite į Notepad ir patikrinkite, ar CF_UNICODETEXT atsarginė kopija yra tvarkingas tekstas su tabuliacijos ženklais, tada tą pačią kopiją įklijuokite į Word arba naršyklę ir patikrinkite, ar rodoma stilizuota versija — jei vienoje programoje turinys atrodo gerai, o kitoje blogai, dažniausiai tai reiškia, kad fragmento žymekliai atsidūrė netinkamoje vietoje. Taip pat laikykite prasmingu, o ne dekoratyviu loginiu rezultatu, kurį grąžina CopyToClipboard: OpenClipboard gali nepavykti, kai kita programa laiko iškarpinę atidarytą, o užimtame darbalaukyje tai pakankamai dažna, kad vienas nepatikrintas iškvietimas galiausiai nieko neįklijuotų be klaidos, paaiškinančios priežastį, todėl toliau pateiktas pakartojimas nuo to apsaugo:
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;
Pats formatas nėra egzotiškas, kai antraštė tiksliai skaičiuojama baitais, o paprasto teksto atsarginė kopija sąžiningai atspindi savo turinį — jis iš esmės nepakitęs gyvuoja nuo tada, kai jį pirmą kartą apibrėžė Internet Explorer, ir visos pagrindinės Windows programos jį tebeperskaito taip pat. CopyToClipboard yra šalia PasteFromClipboard, tos pačios sąveikos skaitymo dalies, platesnėje iškarpinės ir eksporto funkcijų srityje, aprašytoje HotXLS Component produkto puslapyje