Как работают буфер обмена и перетаскивание — правильная обработка OLE-передачи данных в бизнес-приложениях

· · Windows, Буфер обмена, Перетаскивание, OLE, COM, Разработка под Windows, WinForms, WPF

«Когда вставляем таблицу, скопированную из Excel, оформление разваливается. Хотим, чтобы вставлялось как таблица.» «Содержимое, которое копируем в своём приложении, превращается во что-то странное, когда вставляем в Word.» «Хотим уметь принимать файлы перетаскиванием.» — В консультационных разговорах об изменениях бизнес-приложений запросы вокруг копирования-вставки и перетаскивания (D&D) — повседневность.

Именно потому, что это «функции, которые все принимают как должное», как они на самом деле работают, удивительно мало известно. Если думать о буфере обмена как о «коробке, в которую кладут один кусок данных», нельзя объяснить, почему одна и та же копия даёт разные результаты в зависимости от того, куда вставляете, или почему вставка перестаёт работать после того, как закрыли исходное приложение. Настоящий буфер обмена — механизм, который кладёт одно и то же содержимое сразу в нескольких форматах и даёт стороне вставки выбрать формат, который она понимает.

А перетаскивание в основе — OLE-передача данных, которая передаёт ровно то же представление данных, что и буфер обмена (IDataObject), через интерфейсы COM. Иными словами, копирование-вставка и D&D — братья: поймите одно правильно — и другое уже рядом.

Эта статья рассчитана на ИТ-сотрудников малых и средних компаний и на разработчиков приложений Windows. Она связывает в одну картину, как работают форматы буфера обмена, практики на стороне вставки и на стороне копирования, правильный способ следить за буфером обмена, административные политики для истории буфера, облачной синхронизации и RDP, а также структуру и ловушки OLE-перетаскивания.

1. Сначала вывод

  • Буфер обмена — одна область, общая для приложений на одном рабочем столе (станции окон), и то, что там лежит, — не «один кусок данных», а одно и то же содержимое сразу в нескольких форматах. У другого сеанса, например RDP, изначально другой буфер обмена; функция перенаправления как раз мост между ними. Поскольку назначение выбирает формат, который понимает, одна и та же копия даёт разные результаты в зависимости от того, куда вставляете.12
  • Для текста используйте CF_UNICODETEXT. CF_TEXT — ANSI и зависит от кодовой страницы, и на японских системах это рассадник кракозябр. Система неявно преобразует между ними, но каноническая сторона — Unicode.3
  • Файлы путешествуют как CF_HDROP (массив путей, завершённый двойным NUL), а форматированный текст использует зарегистрированный формат «HTML Format». У HTML Format необычная структура: текст UTF-8 с заголовком из байтовых смещений.45
  • Настоящая причина «закрыл исходное приложение и больше не могу вставить» — отложенный рендеринг. Это механизм, который кладёт не полезную нагрузку, а только обещание «произвести, когда спросят»; если пропустить материализацию при выходе (ответ на WM_RENDERALLFORMATS или OleFlushClipboard для OLE), вставка перестаёт работать.26
  • Трактуйте вставленные данные как ненадёжный ввод извне. Сама Microsoft прямо говорит: «данным буфера обмена не доверяют. Разбирайте их осторожно».7
  • Для слежения за буфером обмена единственный вариант — AddClipboardFormatListener + WM_CLIPBOARDUPDATE. Не используйте опрос и не используйте старый SetClipboardViewer (цепочку просмотрщиков). Также предоставлены зарегистрированные форматы, которые не пускают секреты в историю и синхронизацию (ExcludeClipboardContentFromMonitorProcessing и друзья).81
  • История буфера обмена (Win+V) и облачная синхронизация — забота ИТ-управления. Ими можно управлять через AllowClipboardHistory и AllowCrossDeviceClipboard посредством GPO / Intune (Policy CSP), а у перенаправления буфера RDP есть собственная выделенная политика.91011
  • Перетаскивание — это COM. Тот же IDataObject, что и у буфера обмена, передаётся между IDropSource (источник перетаскивания) и IDropTarget (цель сброса) через цикл DoDragDrop. RegisterDragDrop требует инициализации через OleInitialize (STA).1213
  • Нельзя сбросить из Проводника с обычными правами на повышенное приложение. Причина — UIPI (блокировка сообщений по уровню целостности), и это ограничение, которое нужно знать на этапе проектирования.14

Ниже разбираем это от основ буфера обмена вверх.

2. Чем на самом деле является буфер обмена — не «один кусок данных», а «одно содержимое в нескольких форматах»

Буфер обмена — общий механизм обмена данными, до которого может дотянуться каждое приложение, разделяющее один рабочий стол (точнее, он на станцию окон: у другого сеанса пользователя или сеанса RDP свой буфер обмена. Копирование-вставка работает через RDP, потому что функция перенаправления мостит их — глава 7). Первый принцип в том, что он управляется пользователем: официальная проектная позиция — не класть данные и не забирать их за спиной пользователя.1

Важный пункт в том, что копирование не кладёт «один кусок данных». Окно, которое копирует, опустошает буфер обмена и затем кладёт несколько форматов подряд, выражая одно и то же содержимое от более способного формата к менее способному.2 Например, когда копируете таблицу в электронной таблице, концептуально в буфере обмена одновременно лежит что-то вроде следующего.

Приоритет Формат Содержимое
1 Закрытый формат приложения Полное внутреннее представление, включая формулы и оформление (для вставки обратно в то же приложение)
2 HTML Format HTML-фрагмент, который сохраняет структуру таблицы и оформление
3 CSV Текст, разделённый ячейками
4 CF_UNICODETEXT Простой текст, разделённый табуляцией
5 Формат изображения Растр того, как таблица выглядит

Сторона вставки выбирает из этого списка формат, который понимает, и извлекает его. Вставка в Word даёт оформленную таблицу; вставка в Блокнот даёт текст, разделённый табуляцией, — потому что двое выбрали разные форматы. «Результат зависит от того, куда вставляете» — не баг; это нормальное следствие этого проекта.

Почему одна и та же копия даёт разные результаты в зависимости от того, куда вставляетеСторона копирования кладёт одно и то же содержимое в буфер обмена в нескольких форматах, а сторона вставки выбирает формат, который понимает, поэтому Word получает оформленную таблицу, а Блокнот — текст, разделённый табуляциейWordБлокнотКопирование: электронная таблицаБуфер обмена (много форматов)Более богатые форматыБолее простые форматыЗакрытый формат приложенияHTML FormatCSVCF_UNICODETEXTОформленная таблицаТекст, разделённый табуляцией

С другой стороны, жалобы из начала — «оформление разваливается», «вставляется что-то странное» — почти все сводятся к проблеме того, как одна сторона выбирает форматы или как другая сторона их предлагает. Глава 4 покрывает сторону вставки; глава 5 — сторону копирования.

3. Стандартные форматы и зарегистрированные форматы — CF_UNICODETEXT, CF_HDROP, HTML Format

3.1. Стандартные форматы — для текста используйте сторону Unicode

Форматы, которые ОС определяет заранее, называются стандартными форматами. Те, что постоянно появляются в бизнес-приложениях, следующие.3

Формат Значение Содержимое
CF_TEXT 1 Текст ANSI (зависит от кодовой страницы)
CF_UNICODETEXT 13 Текст Unicode. Это канонический формат для текста
CF_HDROP 15 Список путей к файлам (дескриптор HDROP)
CF_DIB 8 Независимый от устройства растр
CF_LOCALE 16 Идентификатор локали, связанный с текстом

CF_TEXT и CF_UNICODETEXT система неявно преобразует друг в друга (синтезированные форматы). Преобразование кодировки использует кодовую страницу, связанную с CF_LOCALE.3 Полагаться на это преобразование отбрасывает символы, которые ANSI не может представить (например, символы только Unicode и комбинирующие символы), поэтому правило — унифицировать то, что приложение читает и пишет, на CF_UNICODETEXT (DataFormats.UnicodeText в .NET).

Неявное преобразование между CF_UNICODETEXT и CF_TEXTПриложение читает и пишет только CF_UNICODETEXT; система синтезирует CF_TEXT неявным преобразованием с кодовой страницей CF_LOCALE. Символы, которые ANSI не может представить, в этом преобразовании отбрасываютсяпреобразование CF_LOCALEПриложение читает и пишетCF_UNICODETEXTCF_TEXT (ANSI)Непредставимые символы отбрасываются

3.2. CF_HDROP — файлы путешествуют как «список путей»

CF_HDROP — то, что используется, когда копируете файлы в Проводнике или когда перетаскиваете файлы. Полезная нагрузка — не сами файлы; это блок памяти, который раскладывает массив, «завершённый двойным NUL»: после заголовка структуры DROPFILES — строки полных путей, разделённые символами NUL, и пустая строка в конце. pFiles заголовка — начальное смещение списка путей, а fWide говорит, являются ли строки 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)

В нативном коде вытаскиваете их по одному через DragQueryFile; в .NET получаете их как string[] через DataFormats.FileDrop. Факт, что «путешествуют только пути, а не сами файлы», снова будет важен в D&D глав 8 и 9.

Раскладка блока памяти CF_HDROPСтруктура DROPFILES сидит в начале глобальной памяти; pFiles — начальное смещение списка путей, а fWide говорит, Unicode ли это. Затем следуют полные пути, разделённые NUL, и блок заканчивается пустой строкой (двойной NUL). Путешествуют только пути, а не сами файлыDROPFILES (pFiles / fWide)C:\\data\\a.txt + NULC:\\data\\b.txt + NULПустая строка (двойной NUL)Путешествуют только пути, не файлы

3.3. Зарегистрированные форматы — RegisterClipboardFormat и «HTML Format»

Для данных, которые стандартные форматы не могут выразить, приложение может выбрать имя и зарегистрировать свой формат. Передайте имя в RegisterClipboardFormat — и получите обратно идентификатор формата; регистрация под тем же именем из другого приложения возвращает тот же идентификатор, поэтому как только вы договорились об имени, можно делиться данными между приложениями.1 Когда передаёте структурированные данные среди собственного набора приложений, используйте имя, которое не столкнётся, например KomuraSoft.Report.RowData.

Представительный зарегистрированный формат — «HTML Format» для форматированного текста (вместе с RTF — один из двух главных форматов богатого текста). Полезная нагрузка — текст UTF-8, но у неё необычная структура: заголовок, который перечисляет байтовые смещения, прикреплён спереди.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>

Каждое смещение — байтовая позиция от начала данных, включая сам заголовок; обычная практика — зарезервировать фиксированную ширину (например, 10 цифр) и записать измеренные значения обратно после того, как построили тело. StartFragment/EndFragment отмечают начало и конец «фрагмента, который пользователь на самом деле выделил» в байтах (не в символах). В UTF-8, который включает японский, число символов и число байт расходятся, поэтому если ошибиться в этом расчёте смещения, вставка в другое приложение отбрасывает начало или конец. Если генерируете HTML Format сами, нужно заполнить заголовок байтовыми позициями, измеренными после кодирования в UTF-8.5

Как заголовок HTML Format связан со смещениямиStartHTML и EndHTML заголовка указывают на весь HTML, а StartFragment и EndFragment — на фрагмент, который выделил пользователь, оба как байтовые позиции от начала данных. Поскольку число символов и число байт в UTF-8 расходятся, заполняйте заголовок байтовыми позициями, измеренными после кодированияЗаголовок (байтовые смещения)Весь HTMLВыделенный фрагментСмещения — байты после UTF-8

CSV (DataFormats.CommaSeparatedValue в .NET) также обычно используется для табличных данных. Для взаимодействия с Excel предложение HTML Format (с оформлением), CSV (только значения) и CF_UNICODETEXT (разделённый табуляцией) вместе значит, что вам не нужно выбирать одно место вставки.

4. Практики на стороне вставки — приоритет форматов и проверка

4.1. Смотрите от богатых форматов вниз

Форматы в буфере обмена выстроены в порядке, в котором сторона копирования их положила (то есть от более выразительных к менее). Базовая линия стороны вставки — смотреть среди форматов, которые вы можете обработать, начиная с того, у которого больше всего информации. В Win32 вы либо перечисляете через EnumClipboardFormats и используете первый формат, который узнаёте, либо передаёте собственный список приоритетов в GetPriorityClipboardFormat и даёте ему выбрать.2

В .NET ветвление выглядит примерно так.

// 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
}

Это ответ на жалобу из начала: «вставка таблицы Excel разваливается». Приложение, которое читает только простой текст, никогда не получает структуру таблицы. Насколько далеко вниз по списку форматов вы принимаете — проектное решение на стороне вставки.

Ветвление вставки, которое смотрит от богатых форматов внизЕсли HTML Format присутствует и полезная нагрузка тоже строка, импортируйте как таблицу; иначе попробуйте CSV; если и того нет, падайте к тексту, разделённому табуляцией. Если ни одного кандидата нет, откажитеданетданетданетНачать вставкуHTML Format + строка?Проверить заголовок → таблицаCSV есть?Импортировать как CSVUnicodeText?Текст, разделённый табуляциейОтказать

4.2. Вставленные данные — внешний ввод

Это легко пропустить, но содержимое буфера обмена — данные извне, и вы не знаете, какое приложение их положило. Microsoft также предупреждает в документации буфера обмена OLE, что «данным буфера обмена не доверяют. Разбирайте их осторожно, прежде чем использовать в приложении».7

  • Проверяйте, что смещения заголовка HTML Format не указывают за пределы буфера (приложения, которые испускают сломанные заголовки, существуют).
  • Значения, которые импортируете как числа, даты или коды, должны проходить ту же проверку, что и экранный ввод.
  • Поставьте защиту от огромных данных. Даже если кто-то вставит изображение в сотни мегабайт или миллионы строк текста, не блокируйте UI и отказывайте, как только предел превышен. Оговорка: GetData .NET в момент вызова материализует всю полезную нагрузку в управляемую строку (и отложенный рендеринг выполняется как часть этого), поэтому проверка размера после GetData — не защита. В Win32 проверка GlobalSize у HGLOBAL, который возвращает GetClipboardData, даёт защиту на стадии «не идти в преобразование и разбор как управляемую строку», но для форматов отложенного рендеринга сам GetClipboardData запускает рендеринг, поэтому предотвратить материализацию на стороне источника копирования всё равно нельзя. Чтобы UI не замерзал, уберите извлечение с UI-потока (и даже тогда, поскольку Clipboard .NET требует STA, делайте это на выделенном потоке, установленном в STA, а не на потоке пула потоков Task.Run (MTA) — раздел 5.1).

Идея, что «значение, которое приходит извне, каким бы путём ни было, проверяется, прежде чем вы его используете», — та же, что изложена в «Никогда не используйте декодированное значение QR-кода как есть». Допущение, что вставка безопасна, потому что это действие пользователя, — то, как начинаются аварии.

Проверяйте вставленные данные, прежде чем использоватьДанные, взятые из буфера обмена, проходят присутствие формата, тип полезной нагрузки, предел размера и проверку содержимого в этом порядке; провал любого из них — отказ или падение к следующему формату-кандидатуне тот типслишком большойнедействительноФормат есть?Тип полезной нагрузки ок?Размер в пределах?Проверить содержимоеИмпортироватьОтказать / следующий формат

5. Практики на стороне копирования — предложение нескольких форматов сразу и отложенный рендеринг

5.1. Кладите несколько форматов сразу

Практика стороны копирования — обратная к 4.1: предлагайте богатый формат и простой формат одновременно. С DataObject WinForms/WPF это можно написать в несколько строк.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

Два замечания. Первое: класс Clipboard .NET можно использовать только из потока STA.15 UI-поток WinForms/WPF — STA из-за [STAThread], поэтому обычно это не проблема, но касаться его из фонового потока падает (основы STA/MTA — в «Основы COM STA/MTA»). Второе: что значит copy: true, связано с отложенным рендерингом в следующем подразделе.

5.2. Отложенный рендеринг — почему «закрой источник — и вставить нельзя»

Строить большую полезную нагрузку во многих форматах каждый раз расточительно, поэтому у буфера обмена есть механизм под названием отложенный рендеринг. Передайте NULL как дескриптор данных в SetClipboardData — и вместо полезной нагрузки регистрируется только обещание «произвести, когда спросят»; когда кто-то запрашивает этот формат, к источнику копирования приходит WM_RENDERFORMAT, и только тогда данные генерируются.2

Следствие этого проекта — «закрыл исходное приложение и больше не могу вставить» из начала. Перед выходом источник копирования получает WM_RENDERALLFORMATS и отвечает за материализацию каждого формата, который ещё не отрендерен; выход без этого — и формат потерян.2

Отложенный рендеринг и почему закрыть-затем-вставить падаетИсточник копирования регистрирует только обещание с дескриптором NULL и материализует по запросу через WM_RENDERFORMAT. При выходе он отвечает за материализацию каждого формата через WM_RENDERALLFORMATS; пропустите это — и формат после закрытия потерянRENDERALLFORMATSПропустить материализациюSetClipboardData NULL = обещаниеСторона вставки запрашиваетWM_RENDERFORMAT → построить сейчасИсточник копирования собирается выйтиВставка работает после выходаФормат потерян после закрытия

На буфере обмена OLE (стиль, который кладёт IDataObject через OleSetClipboard) эта связь ещё яснее. Всё, что держит буфер обмена, — указатель на объект данных, и вызов OleFlushClipboard при выходе приложения материализует данные в буфер обмена, поэтому вставка всё ещё работает после выхода.6 Clipboard.SetDataObject(data, copy: true) .NET как раз задаёт это поведение «сохранить после выхода».

Когда копируете большой диапазон в Excel и пытаетесь выйти, запрос «В буфере обмена большой объём информации. Хотите иметь возможность вставить эту информацию в другую программу позже?» — ровно подтверждение, выполнять ли эту материализацию (flush). Если используете отложенный рендеринг в своём приложении, помните, что материализация при выходе — часть того же комплекта. Отложенный рендеринг — оптимизация производительности, и поскольку запрос рендеринга выполняется синхронно внутри обработки сообщений, у данных, на генерацию которых уходит много времени, есть компромисс заморозки UI.2

6. Практики слежения за буфером обмена — слушатель, повтор и исключение из истории

6.1. Используйте AddClipboardFormatListener

Требования вроде «хотим обнаруживать значение считывателя штрихкодов или копию из учётной системы и импортировать автоматически» нуждаются в слежении за изменениями буфера обмена. Исторически есть три метода; сегодня правильный ответ один.8

Метод Оценка
Читать по таймеру (опрос) Расточительно, и можно пропустить обновления. Не используйте
SetClipboardViewer (цепочка просмотрщиков) Баг в одном приложении цепочки ломает всю цепочку. Сохраняется только для обратной совместимости
AddClipboardFormatListener Рекомендуется. WM_CLIPBOARDUPDATE приходит зарегистрированному окну
Поток слежения за буфером обменаЗарегистрируйтесь через AddClipboardFormatListener, когда создаётся дескриптор, и WM_CLIPBOARDUPDATE приходит, какое бы приложение ни копировало. Читайте с повтором и снимайте регистрацию симметрично через RemoveClipboardFormatListener, когда дескриптор уничтожаетсяснять регистрациюAddClipboardFormatListenerЖдатьКакое-то приложение копируетWM_CLIPBOARDUPDATEЧитать с повтором (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. Повторяйте, когда не можете открыть

Только одно окно за раз может открыть буфер обмена; пока другой процесс держит его открытым, OpenClipboard падает.2 Сразу после WM_CLIPBOARDUPDATE источник копирования или другой наблюдатель часто ещё работает, поэтому временный сбой чтения — нормальное событие. Всегда ставьте несколько повторов с короткой паузой (десятки миллисекунд) между ними. Заметьте: перегрузки Clipboard .NET, которые позволяют указать число повторов и интервал, существуют только на стороне записи, SetDataObject. На стороне чтения (GetDataObject и друзья) эквивалента нет, поэтому цикл «поймать-подождать-повторить» пишете сами — ExternalException в WinForms, COMException в WPF.

Поток повтора чтения буфера обменаТолько одно окно за раз может открыть буфер обмена, поэтому чтение сразу после уведомления об изменении может падать, гоняясь с другим процессом. При исключении подождите десятки миллисекунд и повторите; если упёрлись в предел, сдайтесь на этот раз и подхватите при следующем обновленииуспехзанятоповторпределWM_CLIPBOARDUPDATEПопытаться прочитатьИмпортировать (проверки гл. 4)Подождать десятки мсСдаться на этот раз

6.3. Не пускайте в историю и синхронизацию — забота о функциях копирования, которые работают с секретами

В Windows есть история буфера обмена (Win+V) и межустройственная синхронизация (облачный буфер обмена), и данные, которые кладёт приложение, по умолчанию входят в область обоих. Приложение, которое кладёт секреты вроде паролей или номеров счетов на функцию копирования, также кладёт зарегистрированный формат, который исключает содержимое из истории и синхронизации.1

  • ExcludeClipboardContentFromMonitorProcessing: Положите это — и содержимое этой копии не входит ни в историю, ни в синхронизацию.
  • CanIncludeInClipboardHistory (DWORD 0): Подавить только историю.
  • CanUploadToCloudClipboard (DWORD 0): Подавить только межустройственную синхронизацию.

Причина, по которой пароль, скопированный менеджером паролей, не остаётся на Win+V, — этот механизм. Идентификатор формата получаете, передав имя в RegisterClipboardFormat, и ставите его рядом с обычными данными, поэтому стоит реализовать в любом бизнес-приложении, которое работает с секретами.

7. Буфер обмена с точки зрения ИТ — история, облачная синхронизация и управление RDP

Немного отойдя от разработки, вот пункты, которые важны администратору. История буфера обмена накапливает недавние копии, а облачный буфер синхронизирует копии между устройствами, вошедшими под одной учётной записью Microsoft / Microsoft Entra.10 Как это ни удобно, это также даёт остаток и утечку: личные данные, скопированные из учётной системы, накапливаются в истории, а содержимое, скопированное на рабочем ПК, синхронизируется на личный ПК.

Две политики, которыми это управляют в организации, следующие.

Чем управляете GPO (Конфигурация компьютера > Административные шаблоны > Система > Политики ОС) Policy CSP (Intune) По умолчанию
История буфера обмена Allow Clipboard History Experience/AllowClipboardHistory Разрешено
Межустройственная синхронизация Allow Clipboard synchronization across devices Privacy/AllowCrossDeviceClipboard Разрешено

Обе доступны начиная с Windows 10 версии 1809; отключите их — и соответствующие пункты в приложении «Параметры» становятся серыми, а политика вступает в силу сразу.910

Другая повседневность — перенаправление буфера обмена RDP (удалённый рабочий стол). По умолчанию копирование-вставка работает между локальным ПК и удалённым сеансом, поэтому это может стать путём выноса секретов с сервера. Политика «Не разрешать перенаправление буфера обмена» (значение реестра fDisableClip) может блокировать оба направления.11 Недавние выпуски Windows Server / Windows 11 также добавили более тонкие политики, например ограничить направление сервер-клиент только текстом. Запрещать ли полностью или ограничивать поэтапно — баланс эксплуатации и безопасности.

Пути, по которым содержимое буфера обмена может распространяться, и точки управленияСкопированное содержимое по умолчанию входит в область истории и облачной синхронизации, а на RDP путешествует в другой сеанс через перенаправление. Каждым путём можно управлять политикой, а сторона приложения может исключить себя из истории и синхронизации форматами исключенияБуфер обменаИстория (Win+V)Облачная синхронизацияПеренаправление RDPAllowClipboardHistoryAllowCrossDeviceClipboardfDisableClipФорматы исключения приложения (6.3)

8. Перетаскивание — это COM — IDataObject + IDropSource + IDropTarget

8.1. Те же данные, что у буфера обмена, другой способ нести

OLE-перетаскивание работает со следующими тремя ролями.12

Роль Кто реализует Работа
IDataObject Источник перетаскивания Полезная нагрузка, которую несут. Тот же многоформатный объект данных, что и у буфера обмена
IDropSource Источник перетаскивания Решение, продолжается ли перетаскивание или отменяется, и обратная связь курсора
IDropTarget Цель сброса Объявление принять/отказать в DragEnter/DragOver/DragLeave/Drop и приём сброса

Источник перетаскивания вызывает DoDragDrop, начинается цикл перетаскивания, и когда мышь входит в окно цели сброса, этому IDropTarget уведомляют; при сбросе передаётся IDataObject. Официальная документация также говорит, что «D&D предоставляет ровно ту же функциональность, что копирование-вставка буфера обмена. Если приложение уже реализует копирование-вставку, добавление невелико».12 Иными словами, многоформатный DataObject, который вы построили в главах 2–5, становится полезной нагрузкой D&D как есть.

Поток OLE-перетаскиванияИсточник перетаскивания кладёт IDataObject в полезную нагрузку и вызывает DoDragDrop, чтобы начать цикл перетаскивания; IDropTarget цели сброса объявляет принять/отказать в DragEnter и DragOver, а при Drop выбирает формат из IDataObject и извлекает егоDoDragDropмышь входиткнопка отпущенаIDataObject + IDropSourceЦикл перетаскиванияDragEnter/Over: EffectIDropTarget.DropВыбрать формат и извлечь

8.2. Нужен OleInitialize (STA)

Окно, которое будет целью сброса, регистрируется через RegisterDragDrop, и здесь есть классическая ловушка. Если вы инициализировали COM через CoInitialize/CoInitializeEx, RegisterDragDrop всегда падает с E_OUTOFMEMORY; нужно инициализировать через OleInitialize.13 OleInitialize инициализирует COM как STA, потому что D&D — функция, укоренённая в мире STA окон и цикла сообщений. Вызывающий поток также должен крутить цикл сообщений; пропустите это — и другие приложения зависают во время перетаскивания.13 Фон здесь — ровно обсуждение модели потоков в «Основы COM STA/MTA».

В приложении WinForms/WPF каркас берёт на себя инициализацию OLE и реализации интерфейсов, поэтому разработчику нужно только писать события.

// 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)
    }
};

Форма та же в WPF: получаете через AllowDrop="True" и события DragOver/Drop на элементе и извлекаете массив путей через e.Data.GetData(DataFormats.FileDrop). Объявлять принять/отказать (Effect) на каждом DragEnter/DragOver — соглашение IDropTarget; пропустите — и получите баг, где курсор остаётся на «не разрешено» и никогда не меняется.

9. Ловушки D&D — повышение прав, перемещение и проверка пути

9.1. Нельзя сбросить на приложение, повышенное как администратор

Сбросить файл из Проводника на приложение, запущенное «от имени администратора», и ничего не происходит — это не баг реализации, это поведение ОС. UIPI (User Interface Privilege Isolation) по умолчанию блокирует сообщения от процесса более низкой целостности к окну более высокой целостности, поэтому уведомления о сбросе от Проводника с обычными правами (средняя целостность) никогда не достигают повышенного приложения.14

Как UIPI блокирует сброс на повышенное приложениеУведомления о сбросе от Проводника средней целостности к повышенному приложению высокой целостности по умолчанию блокирует UIPI, и они не приходят. Держите UI на обычных правах и изолируйте привилегированную работу — и сброс приходитуведомление о сбросезаблокированопроходитделегировать привилегированную работуПроводник (средний)UIPIПовышенное приложение: нет сбросаОбычный UI: сброс приходитИзолированный повышенный процесс

Обход, который по отдельности разрешает конкретные сообщения вроде WM_DROPFILES через ChangeWindowMessageFilterEx, хорошо известен,14 но то, что он пропускает, — более старое уведомление о сбросе (WM_DROPFILES); он не решает OLE D&D целиком. Практическое указание ясно: перестаньте проектировать приложение так, чтобы оно постоянно работало с повышенными правами. Изолируйте только ту работу, которой нужно повышение, в отдельный процесс — и сам UI может остаться на обычных правах и принимать D&D (проект изоляции подробно разобран в «Как на практике выделить в Windows-приложении «только те операции, для которых нужны права администратора»»).

9.2. Что значит DragDropEffects — Move — контракт, что «оригинал уходит»

Copy/Move/Link у DragDropEffects — не украшение; это контракт между источником перетаскивания и целью сброса. Источник перетаскивания объявляет набор эффектов, которые разрешает, в DoDragDrop, цель сброса выбирает фактический эффект, и когда Move успешен, источник перетаскивания удаляет данные (файл) — таково соглашение. Если принимающая сторона бездумно возвращает Move, получается авария «сбросил — и исходный файл исчез». Для применения импорта бизнес-приложения принимающая сторона, заявляющая Copy, — безопасное значение по умолчанию.

Контракт DragDropEffects — Move удаляет оригиналИсточник перетаскивания объявляет набор разрешённых эффектов в DoDragDrop, а цель сброса выбирает фактический эффект. Когда Move успешен, источник удаляет файл, поэтому для импорта принимающая сторона должна заявлять CopyCopyMoveИсточник: разрешённые эффектыЦель: выбрать EffectОригинал остаётся (импорт)Источник удаляет файл

9.3. Проверка сброшенного пути

То, что путешествует в CF_HDROP/FileDrop, — только путь (раздел 3.2). Прежде чем импортировать, пропустите его через ту же проверку ненадёжного ввода, что и вставку.

  • Файл или папка: Решите как спецификацию, что происходит, когда сбрасывают целую папку (рекурсивно импортировать или отказать).
  • Заполнители OneDrive: Путь может существовать, пока тело файла не локально — файл по запросу. В момент открытия начинается загрузка, а офлайн она падает. Поведение и контрмеры — в «OneDrive «Files On-Demand» и бизнес-приложения».
  • Длинные пути и необычные пути: Пути длиннее MAX_PATH, сетевые пути (UNC) и пути на съёмных носителях следует принимать только после того, как подтвердили, что последующая обработка с ними справляется.
  • Число и общий размер: Чтобы сброс тысяч файлов не замораживал UI, делайте импорт асинхронным и ставьте предел и отображение прогресса.

10. Итоги

  • Буфер обмена — механизм, который кладёт одно и то же содержимое сразу в нескольких форматах в одной области, общей внутри одного рабочего стола (станции окон). Сторона вставки выбирает формат, поэтому одна и та же копия даёт разные результаты.
  • Текст — CF_UNICODETEXT, файлы — CF_HDROP, форматированный текст — зарегистрированный формат HTML Format (заголовок байтовых смещений + UTF-8).
  • Сторона вставки смотрит от богатого к краткому и трактует полезную нагрузку как внешний ввод. Сторона копирования предлагает несколько форматов сразу, и если использует отложенный рендеринг, реализует также материализацию при выходе (WM_RENDERALLFORMATS / OleFlushClipboard).
  • Слежение — AddClipboardFormatListener + WM_CLIPBOARDUPDATE. Готовьтесь к гонкам OpenClipboard повтором и не пускайте секреты в историю и синхронизацию через ExcludeClipboardContentFromMonitorProcessing и друзей.
  • ИТ может управлять историей буфера обмена, облачной синхронизацией и перенаправлением RDP через GPO / Intune. По умолчанию всё разрешено, поэтому в средах, которые работают с секретами, решайте намеренно.
  • D&D — это COM: IDropSource/IDropTarget передают тот же IDataObject, что и буфер обмена. RegisterDragDrop требует OleInitialize (STA).
  • Сбросы на повышенное приложение блокирует UIPI. Move у DragDropEffects — контракт, что «оригинал уходит»; проверяйте сброшенный путь, прежде чем импортировать.

Копирование-вставка и D&D для пользователя — функции, которые должны ощущаться как воздух. Именно поэтому «не могу вставить», «разваливается» и «исчезло» так бьют по опыту — и почему приложение, которое предлагает несколько форматов и правильно обрабатывает сбросы, само по себе делает повседневные операции глаже. Надеюсь, это полезный материал, когда решаете, что чинить первым.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается проектированием и реализацией поддержки копирования-вставки и перетаскивания в бизнес-приложениях (предложение нескольких форматов, взаимодействие с Excel, импорт сброшенных файлов), расследованием первопричин проблем вроде «разваливается при вставке» или «копия исчезает», автоматизацией ввода, которая следит за буфером обмена, и реализациями, которые не пускают конфиденциальные данные в историю и синхронизацию. Случаи, которые затрагивают нижние слои COM и OLE, приветствуются, даже если начинаете с изоляции симптома.

Справочные ссылки

  1. Microsoft Learn, Clipboard Formats. О том, что окно может класть одну и ту же информацию в нескольких форматах буфера обмена; о зарегистрированных форматах через RegisterClipboardFormat (регистрация того же имени возвращает то же значение, поэтому приложения могут им делиться); о синтезированных форматах; и об исключении содержимого из истории буфера / облачной синхронизации через ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory и CanUploadToCloudClipboard.  2 3 4 5

  2. Microsoft Learn, Clipboard Operations. О том, что только одно окно за раз может открыть буфер обмена; о кладке форматов от более выразительных к менее выразительным в момент копирования; о выборе формата при вставке через EnumClipboardFormats / GetPriorityClipboardFormat; об отложенном рендеринге передачей NULL в SetClipboardData и обязанностях WM_RENDERFORMAT / WM_RENDERALLFORMATS; и о компромиссах отложенного рендеринга.  2 3 4 5 6 7 8

  3. Microsoft Learn, Standard Clipboard Formats. Об определениях стандартных форматов CF_TEXT (ANSI), CF_UNICODETEXT, CF_HDROP, CF_DIB и CF_LOCALE и о том, что система неявно преобразует CF_TEXT и CF_UNICODETEXT, используя кодовую страницу, связанную с CF_LOCALE.  2 3

  4. Microsoft Learn, Shell Clipboard Formats. О том, что CF_HDROP составлен из структуры DROPFILES плюс массив строк полных путей, завершённый двойным NUL; об извлечении отдельных путей через DragQueryFile; и о том, что форматам оболочки CFSTR_ нужна регистрация через RegisterClipboardFormat.  2

  5. Microsoft Learn, HTML Clipboard Format. О том, что зарегистрированное имя — «HTML Format»; о структуре заголовка с байтовыми смещениями вроде Version, StartHTML, EndHTML, StartFragment и EndFragment; о том, что кодировка всегда UTF-8; и о соглашении комментариев StartFragment/EndFragment.  2 3

  6. Microsoft Learn, OleFlushClipboard function (ole2.h). О том, что OleSetClipboard заставляет буфер обмена держать только указатель на объект данных; о том, что OleFlushClipboard материализует данные в буфер обмена, так что вставка всё ещё работает после выхода приложения; и об опустошении буфера обмена через OleSetClipboard(NULL), когда сохранять при выходе не нужно.  2

  7. Microsoft Learn, OleGetClipboard function (ole2.h). О том, как получить IDataObject из буфера обмена, и о предупреждении, что данным буфера обмена не доверяют и их следует осторожно разбирать, прежде чем приложение их использует.  2

  8. Microsoft Learn, Using the clipboard. О сравнении трёх способов слежения за буфером обмена (окна просмотрщиков, порядковые номера и слушатели форматов); о том, что новым программам следует использовать слушателя через AddClipboardFormatListener; о том, что цепочка просмотрщиков хрупка, когда сопровождение цепочки неполно; и о том, что порядковые номера — не то, что следует опрашивать.  2

  9. Microsoft Learn, Policy CSP - Experience. О разрешении или запрете истории буфера обмена политикой Experience/AllowClipboardHistory; о доступности начиная с Windows 10 версии 1809; о том, что по умолчанию разрешено; и о сопоставлении GPO под «Система > Политики ОС» с немедленным вступлением изменений в силу.  2

  10. Microsoft Learn, Policy CSP - Privacy. О разрешении или запрете межустройственной синхронизации буфера обмена политикой Privacy/AllowCrossDeviceClipboard; о том, что синхронизация происходит между устройствами, вошедшими под одной учётной записью Microsoft / Microsoft Entra; и о том, что по умолчанию разрешено.  2 3

  11. Microsoft Learn, Policy CSP - ADMX_TerminalServer. О том, что TS_CLIENT_CLIPBOARD («Не разрешать перенаправление буфера обмена», значение реестра fDisableClip) может запретить обмен буфером между локальным и удалённым в сеансе удалённого рабочего стола, и о том, что перенаправление по умолчанию разрешено.  2

  12. Microsoft Learn, Drag and Drop (COM). О том, что OLE-перетаскивание работает тройкой IDropSource (источник перетаскивания), IDropTarget (цель сброса) и DoDragDrop (цикл, который даёт OLE); о предоставлении той же функциональности, что копирование-вставка буфера обмена, так что приложению, которое уже реализует копирование-вставку, нужно лишь небольшое добавление; и о видах обратной связи.  2 3

  13. Microsoft Learn, RegisterDragDrop function (ole2.h). О регистрации окна цели сброса с IDropTarget; о том, что всегда падает с E_OUTOFMEMORY, если COM инициализировали через CoInitialize/CoInitializeEx, так что нужен OleInitialize; и о том, что приложение источника перетаскивания зависает, если вызывающий поток не крутит цикл сообщений.  2 3

  14. Microsoft Learn, ChangeWindowMessageFilterEx function (winuser.h). О том, что UIPI — механизм безопасности, который по умолчанию блокирует приём сообщений от отправителя более низкой целостности, и о разрешении конкретных сообщений на окно через фильтр сообщений (MSGFLT_ALLOW).  2 3

  15. Microsoft Learn, How to add data to the Clipboard (Windows Forms). О кладке данных сразу в нескольких форматах через DataObject и Clipboard.SetDataObject; о добавлении в нескольких форматах, чтобы другие приложения могли это узнать; и о том, что класс Clipboard можно использовать только из потока STA, так что требуется [STAThread].  2

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Что на самом деле значит «Не отвечает» — как Windows решает, что приложение зависло, и как проектировать приложения, которые не зависают

«Не отвечает» в Windows — механизм, в котором ОС судит, что окно не извлекало сообщение 5 секунд, и подменяет его окном-призраком. Статья...

Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification

Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Почему оформление таблицы, скопированной из Excel, разваливается, когда я вставляю её в своё приложение?
Буфер обмена не держит «один кусок данных». Одно и то же содержимое кладут сразу в нескольких форматах (закрытый формат исходного приложения, HTML Format, CSV, текст Unicode и так далее), и приложение назначения выбирает формат, который понимает, и извлекает его. Когда оформление разваливается, типичная причина в том, что назначение читает только простой текст (CF_UNICODETEXT). Если нужна ещё и структура таблицы, реализуйте сторону вставки так, чтобы она предпочитала HTML Format или CSV. Наоборот, если хотите, чтобы другие приложения корректно вставляли копию, сделанную в вашем приложении, в момент копирования предлагайте и богатый формат, и простой.
Почему я больше не могу вставить после того, как закрыл приложение, из которого копировал?
Потому что источник использует отложенный рендеринг. Приложения, которые работают с большими данными, не кладут полезную нагрузку в момент копирования; они регистрируют в буфере обмена только обещание «произвести, когда спросят». Если источник затем выходит, не материализовав данные в ответ на WM_RENDERALLFORMATS при остановке, любой формат, который ещё не отрендерен, теряется. Приложение, которое использует буфер обмена OLE (IDataObject), может сохранить возможность вставки после выхода, вызвав OleFlushClipboard при остановке, чтобы материализовать данные.
Как моё приложение может следить за изменениями буфера обмена?
Сейчас рекомендуемый метод — зарегистрировать своё окно как слушателя через AddClipboardFormatListener и обрабатывать сообщение WM_CLIPBOARDUPDATE, которое приходит каждый раз, когда содержимое меняется. Опрос содержимого по таймеру тратит работу и может пропускать обновления, а старая цепочка просмотрщиков на основе SetClipboardViewer сохраняется только для обратной совместимости, потому что баг в одном приложении цепочки ломает всю цепочку. Заметьте также, что OpenClipboard при чтении может падать, потому что другой процесс держит буфер обмена, поэтому реализуйте повтор с короткой паузой, если хотите, чтобы чтение было устойчивым.
Есть ли способ не пускать секреты вроде паролей в историю буфера обмена (Win+V)?
Есть два рычага: на стороне приложения и на стороне политики. На стороне приложения, если при копировании вы также кладёте зарегистрированный формат ExcludeClipboardContentFromMonitorProcessing, это содержимое не входит ни в историю, ни в межустройственную синхронизацию. Можно также управлять каждым независимо через CanIncludeInClipboardHistory (только история) и CanUploadToCloudClipboard (только синхронизация). Это механизм, которым пользуются менеджеры паролей. Если хотите выключить для всей организации, можно отключить сами историю и облачную синхронизацию через AllowClipboardHistory и AllowCrossDeviceClipboard посредством групповой политики или Intune (Policy CSP).
Почему я не могу перетащить файл на приложение, которое запущено от имени администратора?
Потому что механизм безопасности под названием UIPI (User Interface Privilege Isolation) блокирует доставку сообщений от процесса более низкой целостности к окну более высокой целостности. Проводник работает с обычными правами (средняя целостность), поэтому уведомления о перетаскивании никогда не достигают окна повышенного приложения. Обход, который по отдельности разрешает сообщения вроде WM_DROPFILES через ChangeWindowMessageFilterEx, хорошо известен, но он применяется только к более старому уведомлению о сбросе. Настоящее исправление — перестать проектировать приложение так, чтобы оно постоянно работало с повышенными правами, и изолировать только ту работу, которой нужно повышение, в отдельный процесс.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог