Come funzionano gli Appunti e il trascinamento della selezione — Gestire correttamente il trasferimento dati OLE nelle app aziendali

· · Windows, Appunti, Drag and drop, OLE, COM, Sviluppo Windows, WinForms, WPF

«Quando incolliamo una tabella copiata da Excel, la formattazione si sfascia. Vogliamo che si incolli come tabella.» «Il contenuto che copiamo nella nostra app diventa qualcosa di strano quando lo incolliamo in Word.» «Vogliamo poter prendere i file in ingresso con il trascinamento della selezione.» — Nelle conversazioni di consulenza sulle modifiche alle app aziendali, le richieste intorno a copia-e-incolla e drag and drop (D&D) sono un classico.

Proprio perché queste sono «funzionalità che tutti danno per scontate», come funzionano davvero è sorprendentemente poco noto. Se pensate agli Appunti come «una scatola in cui mettete un pezzo di dati», non potete spiegare perché la stessa copia produce risultati diversi a seconda di dove incollate, o perché l’incolla smette di funzionare dopo che chiudete l’app di origine. Gli Appunti reali sono un meccanismo che pone lo stesso contenuto in più formati insieme, e lascia al lato incolla scegliere un formato che capisce.

E il drag and drop, in fondo, è trasferimento dati OLE che consegna esattamente la stessa rappresentazione dei dati degli Appunti (IDataObject), attraverso interfacce COM. In altre parole, copia-e-incolla e D&D sono fratelli: capite uno correttamente e l’altro è proprio lì.

Questo articolo è rivolto al personale IT di piccole e medie imprese e agli sviluppatori di app Windows. Lega insieme, in un unico quadro, come funzionano i formati degli Appunti, le pratiche sul lato incolla e sul lato copia, il modo corretto di osservare gli Appunti, le policy amministrative per cronologia degli Appunti, sincronizzazione cloud e RDP, e la struttura e le trappole del drag and drop OLE.

1. Prima di tutto, la conclusione

  • Gli Appunti sono un’unica area condivisa dalle app sullo stesso desktop (window station), e ciò che siede lì non è «un pezzo di dati» ma lo stesso contenuto in più formati insieme. Una sessione diversa, come RDP, ha originariamente Appunti diversi; la funzionalità di reindirizzamento è ciò che fa da ponte tra i due. Poiché la destinazione sceglie un formato che capisce, la stessa copia produce risultati diversi a seconda di dove incollate.12
  • Per il testo, usate CF_UNICODETEXT. CF_TEXT è ANSI e dipendente dalla code page, e sui sistemi giapponesi è un terreno di coltura per il mojibake. Il sistema converte tra i due in modo implicito, ma il lato canonico è Unicode.3
  • I file viaggiano come CF_HDROP (un array di percorsi terminato da doppio NUL), e il testo formattato usa il formato registrato «HTML Format». HTML Format ha una struttura insolita: testo UTF-8 con un’intestazione di offset in byte.45
  • La causa reale di «ho chiuso l’app di origine e non potevo più incollare» è il rendering differito. È un meccanismo che pone non il payload ma solo una promessa di «produrlo quando richiesto»; se saltate la materializzazione all’uscita (rispondere a WM_RENDERALLFORMATS, o OleFlushClipboard per OLE), l’incolla smette di funzionare.26
  • Trattate i dati incollati come input non fidato dall’esterno. Microsoft stessa afferma chiaramente che «i dati degli Appunti non sono fidati. Analizzateli con cura».7
  • Per osservare gli Appunti, AddClipboardFormatListener + WM_CLIPBOARDUPDATE è l’unica opzione. Non usate il polling, e non usate il vecchio SetClipboardViewer (la catena di viewer). Sono previsti anche formati registrati che tengono i segreti fuori da cronologia e sincronizzazione (ExcludeClipboardContentFromMonitorProcessing e compagni).81
  • La cronologia degli Appunti (Win+V) e la sincronizzazione cloud sono una preoccupazione di gestione IT. Potete controllarle con AllowClipboardHistory e AllowCrossDeviceClipboard via GPO / Intune (Policy CSP), e il reindirizzamento degli Appunti RDP ha una policy dedicata propria.91011
  • Il drag and drop è COM. Lo stesso IDataObject degli Appunti viene consegnato tra IDropSource (l’origine del trascinamento) e IDropTarget (la destinazione del rilascio) attraverso il ciclo DoDragDrop. RegisterDragDrop richiede l’inizializzazione con OleInitialize (STA).1213
  • Non potete rilasciare da Esplora file a privilegio ordinario su un’app elevata. UIPI (blocco dei messaggi per livello di integrità) è la causa, ed è un vincolo che dovreste conoscere in fase di progetto.14

Di seguito percorriamo questo dalle fondamenta degli Appunti in su.

2. Cosa sono davvero gli Appunti — Non «un pezzo di dati» ma «lo stesso contenuto in più formati»

Gli Appunti sono un meccanismo comune di condivisione dei dati che ogni app che condivide lo stesso desktop può raggiungere (più precisamente, è per window station: una sessione utente diversa o una sessione RDP ha ciascuna i propri Appunti. Copia-e-incolla funziona su RDP perché la funzionalità di reindirizzamento fa da ponte tra i due — capitolo 7). Il primo principio è che è guidato dall’utente: la posizione ufficiale di progetto è che non mettete dati dentro o li togliete alle spalle dell’utente.1

Il punto importante è che una copia non pone «un pezzo di dati». La finestra che copia svuota gli Appunti e poi pone diversi formati in fila, esprimendo lo stesso contenuto dal formato più capace a quello meno capace.2 Per esempio, quando copiate una tabella in un foglio di calcolo, concettualmente qualcosa come il seguente è sugli Appunti allo stesso tempo.

Priorità Formato Contenuti
1 Formato privato dell’app Una rappresentazione interna completa, incluse formule e formattazione (per incollare di nuovo nella stessa app)
2 HTML Format Un frammento HTML che tiene la struttura della tabella e la formattazione
3 CSV Testo delimitato per celle
4 CF_UNICODETEXT Testo semplice separato da tabulazioni
5 Formato immagine Una bitmap di come appare la tabella

Il lato incolla sceglie un formato che capisce da questo elenco e lo estrae. Incollate in Word e ottenete una tabella formattata; incollate in Blocco note e ottenete testo separato da tabulazioni — perché i due hanno scelto formati diversi. «Il risultato dipende da dove incollate» non è un bug; è la conseguenza normale di questo progetto.

Perché la stessa copia produce risultati diversi a seconda di dove incollateIl lato copia pone lo stesso contenuto sugli Appunti in più formati, e il lato incolla sceglie un formato che capisce, quindi Word ottiene una tabella formattata e Blocco note ottiene testo separato da tabulazioniWordBlocco noteCopia: foglio di calcoloAppunti (molti formati)Formati più ricchiFormati più sempliciPrivato dell'appHTML FormatCSVCF_UNICODETEXTTabella formattataTesto separato da tabulazioni

Detto al contrario, i reclami di apertura — «la formattazione si sfascia», «viene incollato qualcosa di strano» — si riducono quasi tutti a un problema di come un lato sceglie i formati, o come l’altro lato li offre. Il capitolo 4 tratta il lato incolla; il capitolo 5 tratta il lato copia.

3. Formati standard e formati registrati — CF_UNICODETEXT, CF_HDROP, HTML Format

3.1. Formati standard — Usate il lato Unicode per il testo

I formati che il sistema operativo definisce in anticipo si chiamano formati standard. Quelli che comparono di continuo nelle app aziendali sono i seguenti.3

Formato Valore Contenuti
CF_TEXT 1 Testo ANSI (dipendente dalla code page)
CF_UNICODETEXT 13 Testo Unicode. Questo è il formato canonico per il testo
CF_HDROP 15 Un elenco di percorsi di file (un handle HDROP)
CF_DIB 8 Una bitmap indipendente dal dispositivo
CF_LOCALE 16 L’identificatore di locale associato al testo

CF_TEXT e CF_UNICODETEXT vengono convertiti l’uno nell’altro in modo implicito dal sistema (formati sintetizzati). La conversione del codice carattere usa la code page associata a CF_LOCALE.3 Fare affidamento su quella conversione perde i caratteri che ANSI non può rappresentare (per esempio simboli solo Unicode e caratteri combinanti), quindi la regola è unificare ciò che l’app legge e scrive su CF_UNICODETEXT (DataFormats.UnicodeText in .NET).

Conversione implicita tra CF_UNICODETEXT e CF_TEXTL'app legge e scrive solo CF_UNICODETEXT; il sistema sintetizza CF_TEXT per conversione implicita con la code page di CF_LOCALE. I caratteri che ANSI non può rappresentare vengono persi in quella conversioneConversione CF_LOCALEL'app legge e scriveCF_UNICODETEXTCF_TEXT (ANSI)I caratteri non rappresentabili vengono persi

3.2. CF_HDROP — I file viaggiano come «un elenco di percorsi»

CF_HDROP è ciò che si usa quando copiate file in Esplora file, o quando trascinate e rilasciate file. Il payload non sono i file stessi; è un blocco di memoria che dispone un array «terminato da doppio NUL»: dopo un’intestazione di struttura DROPFILES, stringhe di percorso completo separate da caratteri NUL, e una stringa vuota alla fine. Il pFiles dell’intestazione è l’offset di inizio dell’elenco di percorsi, e fWide dice se le stringhe sono Unicode.4

[DROPFILES header: pFiles=start offset of the path list, fWide=1(Unicode)]
C:\data\a.txt(NUL)C:\data\b.txt(NUL)(NUL)

In codice nativo li estraete uno alla volta con DragQueryFile; in .NET li ricevete come un string[] via DataFormats.FileDrop. Il fatto che «ciò che viaggia sono solo i percorsi, non i file stessi» conterà di nuovo nel D&D dei capitoli 8 e 9.

Disposizione del blocco di memoria di CF_HDROPUna struttura DROPFILES siede all'inizio della memoria globale; pFiles è l'offset di inizio dell'elenco di percorsi e fWide dice se è Unicode. Seguono poi i percorsi completi, separati da NUL, e il blocco finisce con una stringa vuota (doppio NUL). Ciò che viaggia sono solo i percorsi, non i file stessiDROPFILES (pFiles / fWide)C:\\data\\a.txt + NULC:\\data\\b.txt + NULStringa vuota (doppio NUL)Viaggiano solo i percorsi, non i file

3.3. Formati registrati — RegisterClipboardFormat e «HTML Format»

Per i dati che i formati standard non possono esprimere, un’app può scegliere un nome e registrare un proprio formato. Passate un nome a RegisterClipboardFormat e ottenete indietro un ID di formato; registrare sotto lo stesso nome da un’app diversa restituisce lo stesso ID, quindi una volta che vi accordate sul nome potete condividere dati tra app.1 Quando passate dati strutturati tra la vostra suite di app, usate un nome che non collida, come KomuraSoft.Report.RowData.

Il formato registrato rappresentativo è «HTML Format», per il testo formattato (insieme a RTF, uno dei due formati rich-text maggiori). Il payload è testo UTF-8, ma ha una struttura insolita: un’intestazione che elenca offset in byte è attaccata davanti.5

Version:0.9
StartHTML:<byte offset of the start of the whole HTML>
EndHTML:<byte offset of the end of the whole HTML>
StartFragment:<byte offset of the start of the fragment>
EndFragment:<byte offset of the end of the fragment>
<html><body>
<!--StartFragment--><b>bold</b> fragment text<!--EndFragment-->
</body></html>

Ogni offset è una posizione in byte dall’inizio dei dati, inclusa l’intestazione stessa; la pratica usuale è riservare una larghezza fissa (per esempio 10 cifre) e scrivere i valori misurati dopo aver costruito il corpo. StartFragment/EndFragment segnano l’inizio e la fine di «il frammento che l’utente ha effettivamente selezionato» in byte (non in caratteri). In UTF-8 che include il giapponese, il conteggio dei caratteri e il conteggio dei byte divergono, quindi se sbagliate questo calcolo di offset, l’incolla in un’altra app perde l’inizio o la fine. Se generate HTML Format voi stessi, dovete riempire l’intestazione con posizioni in byte misurate dopo la codifica in UTF-8.5

Come l'intestazione di HTML Format si relaziona agli offsetStartHTML e EndHTML dell'intestazione puntano all'HTML intero, e StartFragment e EndFragment puntano al frammento selezionato dall'utente, entrambi come posizioni in byte dall'inizio dei dati. Poiché conteggio dei caratteri e conteggio dei byte divergono in UTF-8, riempite l'intestazione con posizioni in byte misurate dopo la codificaIntestazione (offset in byte)HTML interoFrammento selezionatoGli offset sono byte dopo UTF-8

Anche CSV (DataFormats.CommaSeparatedValue in .NET) è comunemente usato per dati tabulari. Per l’interop con Excel, offrire HTML Format (con formattazione), CSV (solo valori) e CF_UNICODETEXT (separato da tabulazioni) insieme significa che non dovete scegliere una singola destinazione di incolla.

4. Pratiche sul lato incolla — Priorità dei formati e convalida

4.1. Guardate dai formati ricchi in giù

I formati sugli Appunti sono allineati nell’ordine in cui il lato copia li ha posti (cioè, dal più espressivo al meno). La linea di base del lato incolla è guardare, tra i formati che potete gestire, partendo da quello con più informazioni. In Win32 o enumerate con EnumClipboardFormats e usate il primo formato che riconoscete, o passate il vostro elenco di priorità a GetPriorityClipboardFormat e lo lasciate scegliere.2

In .NET il ramo assomiglia a qualcosa come il seguente.

// Pasting a table: look from rich to plain
var data = Clipboard.GetDataObject();
if (data is null) return;

// Advertising a format does not guarantee the payload is a string. Use this
// branch only when the type also checks out; otherwise fall through to the next candidate
if (data.GetDataPresent(DataFormats.Html)
    && data.GetData(DataFormats.Html) is string html)
{
    // Validate the HTML Format header, then import as a table
}
else if (data.GetDataPresent(DataFormats.CommaSeparatedValue))
{
    // Import as CSV
}
else if (data.GetDataPresent(DataFormats.UnicodeText))
{
    // Import as tab-separated text
}

È la risposta al reclamo di apertura, «incollare una tabella Excel si sfascia». Un’app che legge solo testo semplice non riceve mai la struttura della tabella. Fino a dove in basso nell’elenco dei formati accettate è una decisione di progetto sul lato incolla.

Ramificazione dell'incolla che guarda dai formati ricchi in giùSe HTML Format è presente e il payload è anche una stringa, importate come tabella; altrimenti provate CSV; se manca anche quello, cadete sul testo separato da tabulazioni. Se nessuno dei candidati è presente, rifiutatenononoInizia l'incollaHTML Format + stringa?Convalida intestazione → tabellaCSV presente?Importa come CSVUnicodeText?Testo separato da tabulazioniRifiuta

4.2. I dati incollati sono input esterno

È facile da perdere, ma i contenuti degli Appunti sono dati dall’esterno, e non sapete quale app li ha posti. Microsoft avverte anche, nella documentazione degli Appunti OLE, che «i dati degli Appunti non sono fidati. Analizzateli con cura prima di usarli nell’app».7

  • Convalidate che gli offset dell’intestazione HTML Format non puntino fuori dal buffer (esistono app che emettono intestazioni rotte).
  • I valori che importate come numeri, date o codici dovrebbero passare attraverso la stessa convalida dell’input a schermo.
  • Mettete una difesa contro i dati enormi. Anche se qualcuno incolla un’immagine di centinaia di megabyte o milioni di righe di testo, non bloccate l’UI, e rifiutate una volta superato un limite. Un’avvertenza: GetData di .NET, nel momento in cui lo chiamate, materializza l’intero payload in una stringa managed (e il rendering differito gira come parte di quello), quindi porre un controllo di dimensione dopo GetData non è una difesa. In Win32, controllare GlobalSize sull’HGLOBAL che GetClipboardData restituisce vi dà una difesa allo stadio di «non procedere nella conversione e nell’analisi come stringa managed», ma per i formati a rendering differito GetClipboardData stesso avvia il rendering, quindi non potete comunque impedire la materializzazione sul lato origine della copia. Per tenere l’UI dal congelarsi, spostate il prelievo fuori dal thread dell’UI (e anche allora, poiché Clipboard di .NET richiede STA, fatelo su un thread dedicato impostato a STA, non sul thread del pool di thread di Task.Run (MTA) — sezione 5.1).

L’idea che «un valore che arriva dall’esterno, qualunque sia il percorso, viene convalidato prima di usarlo» è la stessa esposta in «Non usate mai il valore decodificato di un codice QR così com’è». L’assunto che l’incolla è sicuro perché è un’azione dell’utente è come iniziano gli incidenti.

Convalidate i dati incollati prima di usarliI dati presi dagli Appunti passano attraverso presenza del formato, tipo di payload, limite di dimensione e convalida del contenuto in quell'ordine; fallite uno qualsiasi e rifiutate o cadete sul formato candidato successivotipo sbagliatotroppo grandenon validoFormato presente?Tipo di payload OK?Dimensione entro il limite?Convalida i contenutiImportaRifiuta / formato successivo

5. Pratiche sul lato copia — Offrire più formati insieme, e il rendering differito

5.1. Ponete più formati insieme

La pratica del lato copia è l’inverso di 4.1: offrite un formato ricco e un formato semplice allo stesso tempo. Con il DataObject di WinForms/WPF potete scriverlo in poche righe.15

// WinForms (System.Windows.Forms). WPF is the same shape with System.Windows DataObject/Clipboard
var data = new DataObject();
data.SetData(DataFormats.Html, htmlFormatText);       // HTML Format string including the header
data.SetData(DataFormats.CommaSeparatedValue, csv);   // CSV
data.SetData(DataFormats.UnicodeText, plainText);     // Plain text
Clipboard.SetDataObject(data, copy: true);            // copy:true = keep after the app exits

Due note. Prima, la classe Clipboard di .NET si può usare solo da un thread STA.15 Il thread dell’UI WinForms/WPF è STA a causa di [STAThread], quindi questo normalmente non è un problema, ma toccarla da un thread in background fallisce (i fondamenti STA/MTA sono in «Fondamenti STA/MTA di COM»). Seconda, ciò che significa copy: true è legato al rendering differito nella sottosezione successiva.

5.2. Rendering differito — Perché «chiudi l’origine e non puoi incollare»

Costruire un payload grande in molti formati ogni volta è uno spreco, quindi gli Appunti hanno un meccanismo chiamato rendering differito. Passate NULL come handle dei dati a SetClipboardData e, invece del payload, viene registrata solo una promessa di «produrlo quando richiesto»; quando qualcuno richiede quel formato, WM_RENDERFORMAT arriva all’origine della copia, e solo allora i dati vengono generati.2

La conseguenza di questo progetto è l’apertura «ho chiuso l’app di origine e non potevo più incollare». Prima di uscire, l’origine della copia riceve WM_RENDERALLFORMATS ed è responsabile di materializzare ogni formato che non è ancora stato renderizzato; uscite senza farlo e il formato è perso.2

Rendering differito e perché chiudi-poi-incolla fallisceL'origine della copia registra solo una promessa con un handle NULL, e materializza su richiesta via WM_RENDERFORMAT. All'uscita è responsabile di materializzare ogni formato con WM_RENDERALLFORMATS; saltate quello e il formato è persoRENDERALLFORMATSSalta la materializzazioneSetClipboardData NULL = promessaIl lato incolla lo richiedeWM_RENDERFORMAT → costruisci oraL'origine della copia sta per uscireL'incolla funziona dopo l'uscitaFormato perso dopo la chiusura

Sugli Appunti OLE (lo stile che pone un IDataObject con OleSetClipboard), questa relazione è ancora più chiara. Tutto ciò che gli Appunti tengono è un puntatore all’oggetto dati, e chiamare OleFlushClipboard all’uscita dell’app materializza i dati sugli Appunti, quindi l’incolla funziona ancora dopo l’uscita.6 Clipboard.SetDataObject(data, copy: true) di .NET è ciò che specifica questo comportamento «tienilo dopo l’uscita».

Quando copiate un intervallo grande in Excel e tentate di uscire, il prompt «C’è una grande quantità di informazioni negli Appunti. Volete poter incollare queste informazioni in un altro programma in seguito?» è esattamente la conferma se eseguire questa materializzazione (il flush). Se usate il rendering differito nella vostra app, ricordate che la materializzazione all’uscita fa parte dello stesso insieme. Il rendering differito è un’ottimizzazione di prestazioni, e poiché la richiesta di rendering gira in modo sincrono dentro l’elaborazione dei messaggi, i dati che richiedono molto tempo per essere generati hanno il compromesso di congelare l’UI.2

6. Pratiche per osservare gli Appunti — Listener, retry ed esclusione dalla cronologia

6.1. Usate AddClipboardFormatListener

Requisiti come «vogliamo rilevare un valore da un lettore di codici a barre o una copia dal sistema gestionale e importarlo automaticamente» hanno bisogno che osserviate i cambiamenti degli Appunti. Storicamente ci sono tre metodi; oggi la risposta giusta è una.8

Metodo Valutazione
Lettura su un timer (polling) Spreca, e potete perdere aggiornamenti. Non usate
SetClipboardViewer (catena di viewer) Un bug in un’app della catena rompe l’intera catena. Tenuta solo per compatibilità all’indietro
AddClipboardFormatListener Raccomandato. WM_CLIPBOARDUPDATE arriva alla finestra registrata
Flusso dell'osservazione degli AppuntiRegistrate con AddClipboardFormatListener quando l'handle viene creato, e WM_CLIPBOARDUPDATE arriva qualunque app abbia copiato. Leggete con un retry, e deregistrate in modo simmetrico con RemoveClipboardFormatListener quando l'handle viene distruttoderegistraAddClipboardFormatListenerAttendiQualche app copiaWM_CLIPBOARDUPDATELeggi con retry (6.2)RemoveClipboardFormatListener
// Minimal WinForms implementation
public partial class MainForm : Form
{
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool AddClipboardFormatListener(IntPtr hwnd);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
    const int WM_CLIPBOARDUPDATE = 0x031D;

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        AddClipboardFormatListener(Handle);
    }

    protected override void OnHandleDestroyed(EventArgs e)
    {
        // Unregister symmetrically to match handle destruction / recreation
        RemoveClipboardFormatListener(Handle);
        base.OnHandleDestroyed(e);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_CLIPBOARDUPDATE)
        {
            // Read Clipboard.GetDataObject() here and import if the format is one you need
        }
        base.WndProc(ref m);
    }
}

6.2. Ritentate quando non potete aprirlo

Solo una finestra alla volta può aprire gli Appunti; mentre un altro processo li ha aperti, OpenClipboard fallisce.2 Subito dopo WM_CLIPBOARDUPDATE, l’origine della copia o un altro osservatore sta spesso ancora operando, quindi un fallimento di lettura temporaneo è un evento normale. Mettete sempre alcuni retry con una breve attesa (decine di millisecondi) in mezzo. Si noti che gli overload di Clipboard di .NET che vi consentono di specificare un conteggio di retry e un intervallo esistono solo sul lato scrittura, SetDataObject. Non c’è un equivalente sul lato lettura (GetDataObject e compagni), quindi scrivete voi stessi il catch-attendi-ritenta — ExternalException su WinForms, COMException su WPF.

Flusso di retry della lettura degli AppuntiSolo una finestra alla volta può aprire gli Appunti, quindi una lettura subito dopo la notifica di cambiamento può fallire gareggiando con un altro processo. Su un'eccezione, aspettate decine di millisecondi e ritentate; se colpite il limite, rinunciate questa volta e riprendetelo al prossimo aggiornamentosuccessoin usoritentalimiteWM_CLIPBOARDUPDATETenta una letturaImporta (controlli cap. 4)Attendi decine di msRinuncia questa volta

6.3. Tenetelo fuori da cronologia e sincronizzazione — Cura per le funzionalità di copia che gestiscono segreti

Windows ha la cronologia degli Appunti (Win+V) e la sincronizzazione tra dispositivi (gli Appunti cloud), e i dati che un’app pone sono in ambito per entrambi per default. Un’app che mette segreti come password o numeri di conto su una funzionalità di copia pone anche un formato registrato che esclude il contenuto da cronologia e sincronizzazione.1

  • ExcludeClipboardContentFromMonitorProcessing: Ponete questo e i contenuti di quella copia non sono inclusi né nella cronologia né nella sincronizzazione.
  • CanIncludeInClipboardHistory (DWORD 0): Sopprime solo la cronologia.
  • CanUploadToCloudClipboard (DWORD 0): Sopprime solo la sincronizzazione tra dispositivi.

La ragione per cui una password copiata da un gestore di password non resta su Win+V è questo meccanismo. Ottenete un ID di formato passando il nome a RegisterClipboardFormat e lo impostate a fianco dei dati ordinari, quindi vale la pena implementarlo in qualsiasi app aziendale che gestisce segreti.

7. Gli Appunti dal punto di vista IT — Controlli di cronologia, sincronizzazione cloud e RDP

Allontanandoci un poco dallo sviluppo, ecco i punti che contano per un amministratore. La cronologia degli Appunti accumula le copie recenti, e gli Appunti cloud sincronizzano le copie tra dispositivi connessi con lo stesso account Microsoft / account Microsoft Entra.10 Comodo come è, produce anche residuo e traboccamento: informazioni personali copiate da un sistema gestionale si accumulano nella cronologia, e il contenuto copiato su un PC di lavoro si sincronizza su un PC personale.

Le due policy che usate per controllare questo in un’organizzazione sono le seguenti.

Cosa controllate GPO (Configurazione computer > Modelli amministrativi > Sistema > Criteri del sistema operativo) Policy CSP (Intune) Default
Cronologia degli Appunti Allow Clipboard History Experience/AllowClipboardHistory Consentito
Sincronizzazione tra dispositivi Allow Clipboard synchronization across devices Privacy/AllowCrossDeviceClipboard Consentito

Entrambe sono disponibili da Windows 10 versione 1809 in poi; disabilitatle e le voci corrispondenti nell’app Impostazioni sono disattivate, e la policy ha effetto immediatamente.910

L’altro classico è il reindirizzamento degli Appunti RDP (Desktop remoto). Per default, copia-e-incolla funziona tra il PC locale e la sessione remota, quindi può diventare un percorso per portare segreti via da un server. La policy «Do not allow clipboard redirection» (valore di registro fDisableClip) può bloccare entrambe le direzioni.11 Le versioni recenti di Windows Server / Windows 11 hanno anche aggiunto policy più granulari, come restringere la direzione server-verso-client al solo testo. Se lo vietate in blocco o lo restringete a stadi è un equilibrio di operazioni e sicurezza.

Percorsi lungo cui i contenuti degli Appunti possono diffondersi, e i punti di controlloI contenuti copiati sono in ambito per cronologia e sincronizzazione cloud per default, e su RDP viaggiano verso un'altra sessione via reindirizzamento. Ogni percorso può essere controllato da policy, e il lato app può escludersi da cronologia e sincronizzazione con i formati di esclusioneAppuntiCronologia (Win+V)Sincronizzazione cloudReindirizzamento RDPAllowClipboardHistoryAllowCrossDeviceClipboardfDisableClipFormati di esclusione dell'app (6.3)

8. Il drag and drop è COM — IDataObject + IDropSource + IDropTarget

8.1. Gli stessi dati degli Appunti, un modo diverso di portarli

Il drag and drop OLE gira con i tre ruoli seguenti.12

Ruolo Chi lo implementa Lavoro
IDataObject Origine del trascinamento Il payload che viene portato. Lo stesso oggetto dati multi-formato degli Appunti
IDropSource Origine del trascinamento Decidere se il trascinamento continua o è annullato, e il feedback del cursore
IDropTarget Destinazione del rilascio Dichiarare accetta/rifiuta in DragEnter/DragOver/DragLeave/Drop, e ricevere il rilascio

L’origine del trascinamento chiama DoDragDrop, il ciclo di trascinamento parte, e quando il mouse entra in una finestra destinazione del rilascio viene notificato quell’IDropTarget; al rilascio, l’IDataObject viene consegnato. Anche la documentazione ufficiale dice che «il D&D fornisce esattamente la stessa funzionalità di copia-e-incolla degli Appunti. Se un’app implementa già copia-e-incolla, l’aggiunta è piccola».12 In altre parole, il DataObject multi-formato che avete costruito nei capitoli da 2 a 5 diventa il payload del D&D così com’è.

Flusso del drag and drop OLEL'origine del trascinamento mette un IDataObject nel payload e chiama DoDragDrop per avviare il ciclo di trascinamento; l'IDropTarget della destinazione del rilascio dichiara accetta/rifiuta in DragEnter e DragOver, e su Drop sceglie un formato dall'IDataObject e lo estraeDoDragDropil mouse entrapulsante suIDataObject + IDropSourceCiclo di trascinamentoDragEnter/Over: EffectIDropTarget.DropScegli un formato ed estrai

8.2. OleInitialize (STA) è richiesto

Una finestra che sarà una destinazione del rilascio si registra con RegisterDragDrop, e qui c’è una trappola classica. Se avete inizializzato COM con CoInitialize/CoInitializeEx, RegisterDragDrop fallisce sempre con E_OUTOFMEMORY; dovete inizializzare con OleInitialize.13 OleInitialize inizializza COM come STA, perché il D&D è una funzionalità radicata nel mondo STA di finestre e di una pompa di messaggi. Il thread chiamante deve anche stare girando una pompa di messaggi; saltate quello e altre app si bloccano durante il trascinamento.13 Lo sfondo qui è esattamente la discussione sul modello di threading in «Fondamenti STA/MTA di COM».

In un’app WinForms/WPF il framework si occupa dell’inizializzazione OLE e delle implementazioni delle interfacce, quindi lo sviluppatore deve solo scrivere gli eventi.

// WinForms: accept dropped files
listView1.AllowDrop = true;
listView1.DragEnter += (s, e) =>
{
    // Also check that the source allows Copy (some sources only allow Move/Link)
    e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop)
            && (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy
        ? DragDropEffects.Copy      // Accept: receive as a copy
        : DragDropEffects.None;     // Do not accept
};
listView1.DragDrop += (s, e) =>
{
    // Drag data is also untrusted input. Even if it advertises FileDrop, the payload
    // can be null or a different type, and GetData itself can fail
    object data;
    try { data = e.Data.GetData(DataFormats.FileDrop); }
    catch (COMException) { return; }
    if (data is not string[] paths) return;
    foreach (var path in paths)
    {
        // Validate the path before importing (Section 9.3)
    }
};

La forma è la stessa in WPF: ricevete con AllowDrop="True" e gli eventi DragOver/Drop sull’elemento, e estraete l’array di percorsi con e.Data.GetData(DataFormats.FileDrop). Dichiarare accetta/rifiuta (Effect) a ogni DragEnter/DragOver è la convenzione di IDropTarget; saltatela e ottenete il bug in cui il cursore resta su «non consentito» e non cambia mai.

9. Trappole del D&D — Elevazione, Move e convalida del percorso

9.1. Non potete rilasciare su un’app elevata come amministratore

Rilasciate un file da Esplora file su un’app avviata con «Esegui come amministratore» e non succede niente — questo non è un bug di implementazione, è comportamento del sistema operativo. UIPI (User Interface Privilege Isolation) blocca per default i messaggi da un processo a integrità inferiore a una finestra a integrità superiore, quindi le notifiche di rilascio da Esplora file a privilegio ordinario (integrità media) non raggiungono mai un’app elevata.14

Come UIPI blocca i rilasci su un'app elevataLe notifiche di rilascio da Esplora file a integrità media a un'app elevata a integrità alta sono bloccate da UIPI per default e non arrivano. Tenete l'UI a privilegio ordinario e isolate il lavoro privilegiato, e il rilascio arrivanotifica di rilasciobloccatopassadelega il lavoro privilegiatoEsplora file (media)UIPIApp elevata: nessun rilascioUI ordinaria: il rilascio arrivaProcesso elevato isolato

Una soluzione alternativa che consente individualmente messaggi specifici come WM_DROPFILES con ChangeWindowMessageFilterEx è ben nota,14 ma ciò che lascia passare è la notifica di rilascio più vecchia (WM_DROPFILES); non risolve il D&D OLE nel suo insieme. La guida pratica è chiara: smettete di progettare l’app per girare elevata tutto il tempo. Isolate solo il lavoro che ha bisogno di elevazione in un processo distinto, e l’UI stessa può restare a privilegio ordinario e ricevere D&D (il progetto di isolamento è trattato in dettaglio in «Come isolare concretamente “solo le operazioni che necessitano dei privilegi di amministratore” in un’app Windows»).

9.2. Cosa significa DragDropEffects — Move è un contratto che «l’originale se ne va»

Copy/Move/Link su DragDropEffects non sono decorazione; sono un contratto tra l’origine del trascinamento e la destinazione del rilascio. L’origine del trascinamento dichiara l’insieme di effetti che consente in DoDragDrop, la destinazione del rilascio sceglie l’effetto reale, e quando Move riesce, l’origine del trascinamento elimina i dati (il file) — è la convenzione. Se il lato ricevente restituisce Move senza pensarci, ottenete l’incidente «l’ho rilasciato e il file originale è sparito». Per un uso di import in un’app aziendale, il lato ricevente che dichiara Copy è il default sicuro.

Il contratto di DragDropEffects — Move elimina l'originaleL'origine del trascinamento dichiara l'insieme di effetti consentiti in DoDragDrop, e la destinazione del rilascio sceglie l'effetto reale. Quando Move riesce l'origine del trascinamento elimina il file, quindi per l'import il lato ricevente dovrebbe dichiarare CopyCopyMoveOrigine: effetti consentitiDestinazione: scegli EffectL'originale resta (import)L'origine elimina il file

9.3. Convalidare un percorso rilasciato

Ciò che viaggia in CF_HDROP/FileDrop è solo il percorso (sezione 3.2). Prima di importare, fatelo passare attraverso la stessa convalida di input non fidato dell’incolla.

  • File o cartella: Decidete come specifica cosa succede quando viene rilasciata un’intera cartella (ricorrete e importate, o rifiutate).
  • Segnaposto OneDrive: Il percorso può esistere mentre il corpo del file non è locale — un file on-demand. Nel momento in cui lo aprite parte un download, e offline fallisce. Comportamento e contromisure sono in «OneDrive «File su richiesta» e le app aziendali».
  • Percorsi lunghi e percorsi insoliti: Percorsi oltre MAX_PATH, percorsi di rete (UNC), e percorsi su supporti rimovibili andrebbero accettati solo dopo aver confermato che l’elaborazione a valle può gestirli.
  • Conteggio e dimensione totale: Perché rilasciare migliaia di file non congeli l’UI, fate l’import asincrono e mettete un limite e una visualizzazione del progresso.
  • Gli Appunti sono un meccanismo che pone lo stesso contenuto in più formati insieme in un’unica area condivisa nello stesso desktop (window station). Il lato incolla sceglie il formato, quindi la stessa copia produce risultati diversi.
  • Il testo è CF_UNICODETEXT, i file sono CF_HDROP, e il testo formattato è il formato registrato HTML Format (un’intestazione di offset in byte + UTF-8).
  • Il lato incolla guarda dal ricco al semplice e tratta il payload come input esterno. Il lato copia offre più formati insieme, e se usa il rendering differito implementa anche la materializzazione all’uscita (WM_RENDERALLFORMATS / OleFlushClipboard).
  • L’osservazione è AddClipboardFormatListener + WM_CLIPBOARDUPDATE. Preparatevi alle corse su OpenClipboard con un retry, e tenete i segreti fuori da cronologia e sincronizzazione con ExcludeClipboardContentFromMonitorProcessing e compagni.
  • L’IT può controllare cronologia degli Appunti, sincronizzazione cloud e reindirizzamento RDP con GPO / Intune. Il default è consentito per tutti, quindi decidete deliberatamente negli ambienti che gestiscono segreti.
  • Il D&D è COM: IDropSource/IDropTarget consegnano lo stesso IDataObject degli Appunti. RegisterDragDrop richiede OleInitialize (STA).
  • I rilasci su un’app elevata sono bloccati da UIPI. Move su DragDropEffects è un contratto che «l’originale se ne va»; convalidate un percorso rilasciato prima di importarlo.

Copia-e-incolla e D&D sono, per l’utente, funzionalità che dovrebbero sembrare aria. È esattamente per questo che «non posso incollare», «si sfascia» e «è sparito» feriscono tanto l’esperienza — e perché un’app che offre più formati e gestisce i rilasci in modo corretto rende le operazioni quotidiane più fluide da sola. Spero che questo sia materiale utile quando decidete cosa sistemare per primo.

Articoli correlati

Aree di consulenza correlate

KomuraSoft LLC si occupa del progetto e dell’implementazione del supporto a copia-e-incolla e drag and drop nelle app aziendali (offrire più formati, interop Excel, importare file rilasciati), di indagine sulle cause di problemi come «si sfascia quando incollo» o «la copia sparisce», di automazione dell’input che osserva gli Appunti, e di implementazioni che tengono i dati riservati fuori da cronologia e sincronizzazione. I casi che coinvolgono gli strati inferiori di COM e OLE sono benvenuti anche se partite dall’isolare il sintomo.

Riferimenti

  1. Microsoft Learn, Clipboard Formats. Sul fatto che una finestra può porre la stessa informazione in più formati degli Appunti; sui formati registrati via RegisterClipboardFormat (registrare lo stesso nome restituisce lo stesso valore, quindi le app possono condividerlo); sui formati sintetizzati; e sull’escludere il contenuto da cronologia degli Appunti / sincronizzazione cloud con ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory e CanUploadToCloudClipboard.  2 3 4 5

  2. Microsoft Learn, Clipboard Operations. Sul fatto che solo una finestra alla volta può aprire gli Appunti; sul porre i formati dal più espressivo al meno espressivo al momento della copia; sulla selezione del formato al momento dell’incolla con EnumClipboardFormats / GetPriorityClipboardFormat; sul rendering differito passando NULL a SetClipboardData e sulle responsabilità di WM_RENDERFORMAT / WM_RENDERALLFORMATS; e sui compromessi del rendering differito.  2 3 4 5 6 7 8

  3. Microsoft Learn, Standard Clipboard Formats. Sulle definizioni dei formati standard CF_TEXT (ANSI), CF_UNICODETEXT, CF_HDROP, CF_DIB e CF_LOCALE, e sul fatto che il sistema converte in modo implicito CF_TEXT e CF_UNICODETEXT usando la code page associata a CF_LOCALE.  2 3

  4. Microsoft Learn, Shell Clipboard Formats. Sul fatto che CF_HDROP è composto da una struttura DROPFILES più un array di stringhe di percorso completo terminato da doppio NUL; sul recupero dei singoli percorsi con DragQueryFile; e sul fatto che i formati shell CFSTR_ richiedono la registrazione via RegisterClipboardFormat.  2

  5. Microsoft Learn, HTML Clipboard Format. Sul fatto che il nome registrato è «HTML Format»; sulla struttura dell’intestazione con offset in byte come Version, StartHTML, EndHTML, StartFragment e EndFragment; sul fatto che la codifica è sempre UTF-8; e sulla convenzione dei commenti StartFragment/EndFragment.  2 3

  6. Microsoft Learn, OleFlushClipboard function (ole2.h). Sul fatto che OleSetClipboard fa sì che gli Appunti tengano solo un puntatore all’oggetto dati; sul fatto che OleFlushClipboard materializza i dati sugli Appunti così che l’incolla funziona ancora dopo che l’app esce; e sullo svuotare gli Appunti con OleSetClipboard(NULL) quando non dovete tenerli all’uscita.  2

  7. Microsoft Learn, OleGetClipboard function (ole2.h). Su come ottenere un IDataObject dagli Appunti, e sull’avvertimento che i dati degli Appunti non sono fidati e andrebbero analizzati con cura prima che l’app li usi.  2

  8. Microsoft Learn, Using the clipboard. Sul confronto dei tre modi di osservare gli Appunti (finestre viewer, numeri di sequenza e listener di formato); sul fatto che i programmi nuovi sono tenuti a usare un listener via AddClipboardFormatListener; sul fatto che la catena di viewer è fragile quando la manutenzione della catena è incompleta; e sul fatto che i numeri di sequenza non sono qualcosa che dovreste sondare.  2

  9. Microsoft Learn, Policy CSP - Experience. Sul consentire o negare la cronologia degli Appunti con la policy Experience/AllowClipboardHistory; sulla disponibilità da Windows 10 versione 1809 in poi; sul default che è consentito; e sul mapping GPO sotto «System > OS Policies» con i cambiamenti che hanno effetto immediatamente.  2

  10. Microsoft Learn, Policy CSP - Privacy. Sul consentire o negare la sincronizzazione degli Appunti tra dispositivi con la policy Privacy/AllowCrossDeviceClipboard; sul fatto che la sincronizzazione avviene tra dispositivi connessi con lo stesso account Microsoft / account Microsoft Entra; e sul default che è consentito.  2 3

  11. Microsoft Learn, Policy CSP - ADMX_TerminalServer. Sul fatto che TS_CLIENT_CLIPBOARD («Do not allow clipboard redirection», valore di registro fDisableClip) può vietare la condivisione degli Appunti tra locale e remoto in una sessione Desktop remoto, e sul fatto che il reindirizzamento è consentito per default.  2

  12. Microsoft Learn, Drag and Drop (COM). Sul fatto che il drag and drop OLE gira con i tre di IDropSource (origine del trascinamento), IDropTarget (destinazione del rilascio) e DoDragDrop (il ciclo che OLE fornisce); sul fornire la stessa funzionalità di copia-e-incolla degli Appunti, così che un’app che implementa già copia-e-incolla ha bisogno solo di una piccola aggiunta; e sui tipi di feedback.  2 3

  13. Microsoft Learn, RegisterDragDrop function (ole2.h). Sul registrare una finestra destinazione del rilascio con un IDropTarget; sul fallire sempre con E_OUTOFMEMORY se COM è stato inizializzato con CoInitialize/CoInitializeEx, così che OleInitialize è richiesto; e sul fatto che l’app origine del trascinamento si blocca se il thread chiamante non sta girando una pompa di messaggi.  2 3

  14. Microsoft Learn, ChangeWindowMessageFilterEx function (winuser.h). Sul fatto che UIPI è un meccanismo di sicurezza che per default blocca la ricezione di messaggi da un mittente a integrità inferiore, e sul consentire messaggi specifici per finestra con un filtro di messaggi (MSGFLT_ALLOW).  2 3

  15. Microsoft Learn, How to add data to the Clipboard (Windows Forms). Sul porre dati in più formati insieme con DataObject e Clipboard.SetDataObject; sull’aggiungere in più formati così che altre app possano riconoscerlo; e sul fatto che la classe Clipboard è utilizzabile solo da un thread STA, così che [STAThread] è richiesto.  2

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Perché la formattazione di una tabella copiata da Excel si sfascia quando la incollo nella mia app?
Gli Appunti non tengono «un pezzo di dati». Lo stesso contenuto è posto in diversi formati insieme (il formato privato dell'app di origine, HTML Format, CSV, testo Unicode, e così via), e l'app di destinazione sceglie un formato che capisce e estrae quello. Quando la formattazione si sfascia, la causa tipica è che la destinazione sta leggendo solo testo semplice (CF_UNICODETEXT). Se volete anche la struttura della tabella, implementate il lato incolla così che preferisca HTML Format o CSV. Viceversa, se volete che altre app incollino correttamente da una copia fatta nella vostra app, offrite sia un formato ricco sia un formato semplice al momento della copia.
Perché non posso più incollare dopo aver chiuso l'app da cui ho copiato?
Perché l'origine sta usando il rendering differito. Le app che gestiscono dati grandi non pongono il payload al momento della copia; registrano sugli Appunti solo una promessa che «lo produrranno quando richiesto». Se l'origine poi esce senza materializzare i dati in risposta a WM_RENDERALLFORMATS allo spegnimento, qualsiasi formato che non è ancora stato renderizzato è perso. Un'app che usa gli Appunti OLE (IDataObject) può tenere l'incolla funzionante dopo l'uscita chiamando OleFlushClipboard allo spegnimento per materializzare i dati.
Come può la mia app osservare i cambiamenti degli Appunti?
Il metodo attualmente raccomandato è registrare la vostra finestra come listener con AddClipboardFormatListener e gestire il messaggio WM_CLIPBOARDUPDATE che arriva ogni volta che i contenuti cambiano. Sondare i contenuti con un timer spreca lavoro e può perdere aggiornamenti, e la vecchia catena di viewer basata su SetClipboardViewer è tenuta solo per compatibilità all'indietro, perché un bug in un'app della catena rompe l'intera catena. Si noti anche che OpenClipboard in una lettura può fallire perché un altro processo tiene gli Appunti, quindi implementate un retry con una breve attesa se volete che la lettura sia stabile.
C'è un modo per tenere segreti come le password fuori dalla cronologia degli Appunti (Win+V)?
Ci sono due leve, una sul lato app e una sul lato policy. Sul lato app, se ponete anche il formato registrato ExcludeClipboardContentFromMonitorProcessing quando copiate, quel contenuto non è incluso né nella cronologia né nella sincronizzazione tra dispositivi. Potete anche controllare ciascuno in modo indipendente con CanIncludeInClipboardHistory (solo cronologia) e CanUploadToCloudClipboard (solo sincronizzazione). È il meccanismo che usano i gestori di password. Se volete spegnerlo per l'intera organizzazione, potete disabilitare cronologia e sincronizzazione cloud stesse con AllowClipboardHistory e AllowCrossDeviceClipboard via Criteri di gruppo o Intune (Policy CSP).
Perché non posso trascinare e rilasciare un file su un'app che gira come amministratore?
Perché un meccanismo di sicurezza chiamato UIPI (User Interface Privilege Isolation) blocca la consegna di messaggi da un processo a integrità inferiore a una finestra a integrità superiore. Esplora file gira a privilegio ordinario (integrità media), quindi le notifiche di drag and drop non raggiungono mai la finestra di un'app elevata. Una soluzione alternativa che consente individualmente messaggi come WM_DROPFILES con ChangeWindowMessageFilterEx è ben nota, ma si applica solo alla notifica di rilascio più vecchia. La vera correzione è smettere di progettare l'app per girare elevata tutto il tempo, e isolare solo il lavoro che ha bisogno di elevazione in un processo distinto.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog