Copia un intervallo da una griglia Delphi e incollalo in Word, e la formattazione di solito svanisce: testo semplice, senza intestazioni in grassetto, senza bordi, senza riempimenti. HotXLS colma questo divario con TXLSRange.CopyToClipboard, che colloca un payload clipboard CF_HTML — il formato Windows per HTML con stile e marcatori di frammento esatti a livello di byte — sulla clipboard accanto al testo Unicode semplice
Suona semplice finché non guardi cosa richiede realmente un payload CF_HTML. Il formato richiede una breve intestazione testuale che nomini esattamente dove il frammento inizia e finisce all'interno del buffer clipboard più ampio, e quelle posizioni sono offset di byte, contati attraverso qualunque codifica multi-byte in cui l'HTML finisca per trovarsi. Sbaglia l'aritmetica anche solo di un byte e l'applicazione di destinazione o afferra la sezione sbagliata di markup oppure rinuncia e ripiega su testo semplice, e nessuno dei due fallimenti sembra un bug nel tuo codice — sembra Word che fa il suo Word
Perché copia-incolla da una griglia Delphi di solito perde la propria formattazione
La chiamata clipboard Windows predefinita a cui la maggior parte del codice Delphi ricorre, SetClipboardData con CF_TEXT o CF_UNICODETEXT, porta solo mai caratteri semplici, quindi qualsiasi stile applicato nella griglia sorgente non ha dove andare. Word, Outlook e ogni browser basato su Chromium cercano un formato più ricco quando incolli: una rappresentazione HTML della selezione, completa di stili inline, struttura tabellare e link. Excel stesso si affida esattamente a questo trucco — copia un intervallo in Excel e la clipboard riceve silenziosamente diversi formati contemporaneamente, HTML tra questi, cosicché qualunque applicazione in cui incolli scelga il più ricco che comprende. Un componente che scrive solo mai CF_UNICODETEXT non offre nulla su cui lavorare a nessuno di quei consumatori più ricchi, e la ricchezza visiva che l'utente ha appena copiato semplicemente non c'è da incollare
Cos'è esattamente il formato clipboard CF_HTML?
CF_HTML non è un formato clipboard di sistema fisso come CF_TEXT; è uno registrato dinamicamente, richiesto per nome tramite RegisterClipboardFormat('HTML Format'), e il suo payload è una breve intestazione ASCII seguita da un documento o frammento HTML. L'intestazione porta cinque campi — Version, StartHTML, EndHTML, StartFragment, EndFragment — dove Version è sempre 0.9 e gli altri quattro sono numeri decimali scritti come cifre ASCII. StartHTML ed EndHTML delimitano l'intero documento come l'applicazione ricevente dovrebbe analizzarlo per contesto, font e stili inclusi, mentre StartFragment ed EndFragment delimitano la sezione più ristretta che finisce realmente al cursore, convenzionalmente segnata nel markup stesso con commenti <!--StartFragment--> e <!--EndFragment--> cosicché i confini sopravvivano a una riserializzazione ingenua
Offset di byte, non conteggi di caratteri: la classica trappola di CF_HTML
I quattro campi numerici dell'intestazione di CF_HTML sono offset di byte nell'esatta sequenza di byte che si trova sulla clipboard, contati dal primissimo carattere dell'intestazione stessa — non conteggi di caratteri, non code point Unicode, e non offset relativi al frammento o al tag <body>. Quella distinzione è dove le implementazioni CF_HTML scritte a mano sbagliano silenziosamente: la Length di una UnicodeString Delphi riporta unità di codice UTF-16, che per caso equivalgono al conteggio di byte per il testo ASCII semplice, quindi il bug passa pulito attraverso qualsiasi test scritto con dati di esempio in inglese e si manifesta solo una volta che una cella copiata contiene una lineetta em, un simbolo di valuta o un carattere accentato — un simbolo dell'euro è un'unità di codice UTF-16 ma tre byte in UTF-8, e ogni offset calcolato dopo quel punto deriva di quanti byte extra la codifica ha aggiunto. Il fallimento che segue non è un crash; è l'applicazione ricevente che afferra esattamente l'intervallo di byte a cui l'intestazione puntava, trova una sezione di markup che inizia o finisce a metà tag, e o renderizza spazzatura oppure rinuncia e ripiega su qualunque testo semplice si trovi accanto ad essa sulla clipboard, silenziosamente, senza nulla nel tuo codice a spiegare perché — ecco la forma di codice che produce esattamente quel fallimento:
// 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;
Come HotXLS mantiene l'intestazione accurata a livello di byte
HotXLS evita strutturalmente questa classe di bug: TXLSRange.CopyToClipboard e l'unit lxClipboard sottostante costruiscono il documento CF_HTML e la sua intestazione interamente come AnsiString, il tipo stringa di byte di Delphi, cosicché Length e Pos restituiscano già posizioni di byte ovunque nel calcolo — non c'è un passaggio separato, e quindi nessun passaggio da dimenticare, in cui un conteggio di caratteri Unicode dovrebbe essere convertito in un conteggio di byte prima di entrare nell'intestazione
C'è un secondo trucco, più piccolo, che vale la pena conoscere se mai costruisci a mano un'intestazione CF_HTML. L'intestazione viene scritta due volte: una volta con dieci cifre zero al posto di ciascuno dei quattro offset, cosicché la propria lunghezza in byte possa essere misurata, e una seconda volta con i veri offset inseriti. Poiché ogni vero offset è formattato con quella stessa larghezza fissa a dieci cifre, la seconda intestazione risulta byte per byte della stessa lunghezza della versione segnaposto, ed è esattamente per questo che la misurazione precedente resta valida dopo la riscrittura. Salta la larghezza fissa, formatta un numero con un semplice IntToStr invece, e l'intestazione può restringersi o crescere di una cifra tra i due passaggi, invalidando silenziosamente ogni offset che la segue:
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;
Perché il payload di testo semplice deve comunque viaggiare insieme
TXLSRange.CopyToClipboard non colloca mai CF_HTML da solo sulla clipboard; scrive sempre CF_UNICODETEXT nella stessa chiamata, perché CF_HTML è un formato registrato piuttosto che una delle costanti CF_* fisse che ogni applicazione Windows già sa cercare — un semplice editor di testo, una griglia legacy, o qualsiasi cosa che non abbia mai controllato 'HTML Format' non lo vedrà affatto, e l'intervallo che hai copiato o arriva come testo delimitato da tabulazioni oppure non arriva. Quel testo delimitato da tabulazioni non è nemmeno un'approssimazione grossolana: le celle con formula si copiano come la propria stringa di formula con un = iniziale ripristinato se il testo memorizzato lo aveva perso, corrispondendo a come si comporta il testo clipboard proprio di Excel, le celle ordinarie copiano il proprio FormattedText — la stringa come visualizzata, quindi una cella di valuta si copia come $1.234,56, non il valore sottostante 1234.56 — e qualsiasi campo contenente una tabulazione, una virgoletta o un'interruzione di riga viene racchiuso tra virgolette con le virgolette interne raddoppiate, la stessa convenzione che usa CSV
SaveAsHTML non è un percorso di rendering separato aggiunto solo per il caso della clipboard. CopyToClipboard chiama esattamente lo stesso scrittore HTML descritto in l'esportazione CSV, TSV e HTML di HotXLS, quindi avvolge qualunque cosa quello scrittore produca nell'involucro CF_HTML invece di salvarlo come file autonomo, cosicché tutto ciò che è vero per quell'HTML si trasmetta direttamente a ciò che finisce sulla clipboard. Riunire un intervallo di foglio di lavoro come entrambi i formati in un'unica chiamata si presenta così:
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;
L'intervallo incollato mantiene i propri font, colori e celle unite?
Sì, perché la metà HTML del payload è un rendering completo dell'intervallo, non un semplice scarico di dati: font, colori di riempimento, bordi, formati numerici e celle unite arrivano tutti come stili inline e struttura tabellare, gli stessi meccanismi di stile trattati nella guida di HotXLS alla formattazione condizionale e al rich text, poiché sia i run di rich text di una cella sia il risultato della formattazione condizionale alimentano lo stesso rendering da cui CopyToClipboard legge. Ciò che non sopravvive al viaggio è il comportamento di formula viva: la forma testuale semplice di una cella con formula porta la stringa di formula, quindi una destinazione di incolla consapevole di fogli di calcolo potrebbe in linea di principio ricalcolarla, ma la forma HTML porta sempre solo l'ultimo risultato calcolato, perché HTML non ha alcun concetto di formula che un browser o un elaboratore di testo possa valutare
Verificare l'incolla e gestire una clipboard occupata
Due abitudini catturano la maggior parte dei problemi di clipboard prima che lo faccia un cliente. Incolla prima in Notepad per confermare che il fallback CF_UNICODETEXT sia sano testo delimitato da tabulazioni, poi incolla la stessa copia in Word o in un browser per confermare che la versione con stile compaia — un payload che appare corretto nell'uno e sbagliato nell'altro di solito significa che i marcatori di frammento sono finiti nel posto sbagliato. Poi tratta il risultato Booleano che CopyToClipboard restituisce come significativo, non decorativo: OpenClipboard può fallire quando un altro processo tiene la clipboard aperta, abbastanza comune su un desktop occupato da far sì che una chiamata non controllata alla fine non incolli nulla senza alcun errore a spiegare perché, che è ciò da cui protegge il retry qui sotto:
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;
Il formato in sé non è esotico una volta che l'intestazione è accurata a livello di byte e il fallback in testo semplice è onesto su cosa contiene — è esistito in gran parte invariato da quando Internet Explorer lo ha definito per primo, e ogni applicazione Windows importante lo legge ancora nello stesso modo. CopyToClipboard risiede accanto a PasteFromClipboard, il lato di lettura dello stesso scambio, nella più ampia superficie di clipboard ed esportazione documentata sulla pagina prodotto del componente HotXLS