Articolo tecnico

Implementare il Formato Clipboard CF_HTML in Delphi

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

Diagramma di un carico utile CF_HTML negli appunti costruito da HotXLS in Delphi, che mostra l'intestazione ASCII a cinque campi e il documento UTF-8 con i marcatori di commento StartFragment ed EndFragment
La busta CF_HTML è un breve header ASCII davanti a un documento UTF-8, con la porzione incollata marcata dai commenti StartFragment e EndFragment

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() su una UnicodeString conta unità di codice UTF-16, non byte
var
  Header: string;
  Fragment: string;
  StartFragmentOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
  StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
  // Un simbolo di valuta, una lineetta em, o qualsiasi carattere accentato posto
  // prima di questo punto costa qui un carattere ma due o tre byte
  // una volta che il documento è codificato UTF-8, quindi StartFragmentOfs ora punta
  // un po' prima del punto in cui il frammento inizia davvero sulla clipboard reale
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

Diagramma che contrappone i conteggi di unità di codice UTF-16 agli offset di byte UTF-8 in un'intestazione CF_HTML Delphi, dove caratteri accentati e un simbolo euro fanno derivare i confini dei frammenti
Un solo carattere multi-byte sposta ogni offset di byte calcolato dopo di esso, così HotXLS misura l'intero header in byte AnsiString anziché in code unit

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 cifre ASCII: larghezza fissa in ingresso, larghezza fissa in uscita
var
  Header: AnsiString;           // AnsiString.Length è un conteggio di byte, non un conteggio di caratteri
  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);   // sicuro da misurare una volta, in anticipo
  // ...calcola gli offset reali sul documento AnsiString...
  // poi ricostruisci Header con i numeri reali formattati alla stessa
  // larghezza di 10 cifre, così la sua lunghezza in byte -- e quindi StartHtmlOfs --
  // non cambia mai tra il passaggio segnaposto e quello finale
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

Diagramma di HotXLS CopyToClipboard che scrive CF_HTML e CF_UNICODETEXT negli appunti di Windows così Word e i browser incollano tabelle stilizzate mentre gli editor semplici ricevono testo delimitato da tabulazioni
CopyToClipboard scrive sempre una metà in testo semplice accanto all'HTML, così ogni destinazione da Word a Notepad riceve qualcosa di onesto

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');
    // Gli intervalli classici TXLSWorkbook espongono il metodo identico di
    // 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);   // dai un momento all'app che tiene la clipboard
  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