クリップボードとドラッグ&ドロップの仕組み ── OLEデータ転送を業務アプリで正しく扱う

· · Windows, クリップボード, ドラッグアンドドロップ, OLE, COM, Windows開発, WinForms, WPF

「Excelからコピーした表を貼り付けると、書式が崩れる。表として貼れるようにしてほしい」「うちのアプリでコピーした内容が、Wordに貼ると変なものになる」「ファイルをドラッグ&ドロップで取り込めるようにしてほしい」── 業務アプリの改修相談で、コピー&ペーストとドラッグ&ドロップ(D&D)にまつわる要望は定番です。

これらは”当たり前の機能”であるだけに、仕組みが意外と知られていません。クリップボードを「1つのデータを入れる箱」だと思っていると、同じコピーなのに貼り付け先によって結果が変わる理由も、コピー元アプリを閉じたら貼れなくなる理由も説明できません。実際のクリップボードは、同じ内容を複数の形式で同時に置き、貼り付け側が自分の理解できる形式を選ぶ仕組みです。

そしてドラッグ&ドロップの正体は、クリップボードとまったく同じデータ表現(IDataObject)を、COMのインターフェース越しに受け渡すOLEのデータ転送です。つまりコピペとD&Dは兄弟であり、片方を正しく理解すればもう片方はすぐそこにあります。

この記事では、中小企業の情シス担当者とWindowsアプリ開発者を対象に、クリップボードの形式の仕組み、貼り付ける側・コピーする側それぞれの作法、監視の正しい方法、クリップボード履歴・クラウド同期・RDPの管理ポリシー、そしてOLEドラッグ&ドロップの構造と落とし穴までを一枚につなぎます。

1. まず結論

  • クリップボードは同じデスクトップ(ウィンドウステーション)内のアプリで共有される1つの領域で、そこには「1つのデータ」ではなく同じ内容が複数の形式で同時に置かれます。RDPなど別セッションとの間は本来別のクリップボードで、リダイレクト機能が橋渡ししています。貼り付け先が理解できる形式を選ぶため、同じコピーでも貼り付け先によって結果が変わります。12
  • テキストはCF_UNICODETEXTを使います。CF_TEXTはANSIでコードページ依存であり、日本語環境では文字化けの温床です。両者はシステムが暗黙変換しますが、正はUnicode側です。3
  • ファイルの受け渡しはCF_HDROP(二重NUL終端のパス配列)、書式付きテキストは登録形式「HTML Format」です。HTML Formatはバイトオフセットのヘッダーを持つUTF-8テキストという独特の構造をしています。45
  • 「コピー元アプリを閉じたら貼れなくなった」の正体は遅延レンダリングです。データ本体ではなく「要求されたら作る」約束だけを置く仕組みで、終了時に確定(WM_RENDERALLFORMATSへの応答、OLEならOleFlushClipboard)を怠ると貼り付けできなくなります。26
  • 貼り付けるデータは外部由来の信用できない入力として扱います。Microsoft自身が「クリップボードのデータは信頼できない。注意深くパースせよ」と明言しています。7
  • 監視はAddClipboardFormatListener+WM_CLIPBOARDUPDATE一択です。ポーリングや旧来のSetClipboardViewer(ビューアーチェーン)は使いません。機密を履歴・同期に載せない登録形式(ExcludeClipboardContentFromMonitorProcessing等)も用意されています。81
  • クリップボード履歴(Win+V)とクラウド同期は情シスの管理対象です。GPO/Intune(Policy CSP)のAllowClipboardHistory・AllowCrossDeviceClipboardで制御でき、RDPのクリップボードリダイレクトにも専用ポリシーがあります。91011
  • ドラッグ&ドロップの正体はCOMです。クリップボードと同じIDataObjectを、IDropSource(ドラッグ元)とIDropTarget(ドロップ先)がDoDragDropのループ越しに受け渡します。RegisterDragDropにはOleInitialize(STA)での初期化が必須です。1213
  • 昇格したアプリへは通常権限のエクスプローラーからドロップできません。UIPI(整合性レベルによるメッセージ遮断)が原因で、設計段階で知っておくべき制約です。14

以下、クリップボードの土台から順に見ていきます。

2. クリップボードの正体 ── 「1つのデータ」ではなく「同じ内容の複数形式」

クリップボードは、同じデスクトップを共有するすべてのアプリからアクセスできる、共通のデータ共有の仕組みです(正確にはウィンドウステーション単位で、別のユーザーセッションやRDPセッションはそれぞれ別のクリップボードを持ちます。RDPでコピペが通るのは、リダイレクト機能が両者を橋渡ししているからです ── 7章)。ユーザー主導で使うことが大原則で、ユーザーの知らないところで勝手にデータを出し入れしない、というのが公式の設計方針です。1

重要なのは、コピー操作で置かれるのが「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側を使う

OSがあらかじめ定義している形式を標準形式と呼びます。業務アプリで頻出するのは次の顔ぶれです。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(.NETならDataFormats.UnicodeText)に統一するのが原則です。

CF_UNICODETEXTとCF_TEXTの暗黙変換アプリはCF_UNICODETEXTだけを読み書きし、CF_TEXTはシステムがCF_LOCALEのコードページで暗黙変換して合成する。ANSI側で表現できない文字はこの変換で落ちるシステムが暗黙変換(CF_LOCALEのコードページ)アプリの読み書きCF_UNICODETEXT(正)CF_TEXT(ANSI・コードページ依存)表現できない文字は変換で落ちる(文字化けの温床)

3.2. CF_HDROP ── ファイルは「パスの一覧」で渡る

エクスプローラーでファイルをコピーしたときや、ファイルをD&Dしたときに使われるのがCF_HDROPです。中身はファイル本体ではなく、DROPFILES構造体のヘッダーに続けて、フルパス文字列をNUL文字で区切り、最後に空文字列を置いた「二重NUL終端」の配列を並べたメモリブロックです。ヘッダーのpFilesがパスリストの開始オフセットを、fWideが文字列がUnicodeかどうかを示します。4

[DROPFILESヘッダー: pFiles=パスリストの開始オフセット, fWide=1(Unicode)]
C:\data\a.txt(NUL)C:\data\b.txt(NUL)(NUL)

ネイティブコードではDragQueryFileで1件ずつ取り出し、.NETではDataFormats.FileDropとしてstring[]で受け取れます。「渡るのはパスだけで、ファイル本体ではない」という点は、8〜9章のD&Dでも効いてきます。

CF_HDROPのメモリブロック構造グローバルメモリの先頭にDROPFILES構造体があり、pFilesがパスリストの開始オフセットを、fWideがUnicodeかどうかを示す。続いてNUL区切りのフルパスが並び、最後は空文字列による二重NUL終端で終わる。渡るのはパスだけでファイル本体ではないDROPFILES構造体(pFiles = リスト開始オフセット / fWide = 1)C:\\data\\a.txt + NULC:\\data\\b.txt + NUL終端の空文字列(二重NUL)渡るのはパスだけで、ファイル本体ではない

3.3. 登録形式 ── RegisterClipboardFormatと「HTML Format」

標準形式で表現できないデータのために、アプリは名前を決めて独自形式を登録できます。RegisterClipboardFormatに名前を渡すと形式IDが返り、同じ名前で登録すれば別のアプリでも同じIDが返るため、名前さえ合意すればアプリ間でデータを共有できます。1 自社アプリ群の間で構造化データを受け渡す場合は、KomuraSoft.Report.RowDataのように衝突しない名前を付けます。

登録形式の代表が、書式付きテキスト用の「HTML Format」です(RTFと並ぶ2大リッチテキスト形式です)。中身はUTF-8のテキストですが、先頭にバイトオフセットを列挙したヘッダーが付くという独特の構造をしています。5

Version:0.9
StartHTML:<HTML全体の開始バイト位置>
EndHTML:<HTML全体の終了バイト位置>
StartFragment:<断片の開始バイト位置>
EndFragment:<断片の終了バイト位置>
<html><body>
<!--StartFragment--><b>太字の</b>断片テキスト<!--EndFragment-->
</body></html>

各オフセットはヘッダー自身を含むデータ先頭からのバイト位置で、固定桁(例: 10桁)で領域を確保しておき、本文を組み立てた後に実測値を書き戻すのが定石です。StartFragment/EndFragmentは「ユーザーが実際に選択した断片」の開始・終了をバイト単位で示します(文字単位ではありません)。日本語を含むUTF-8では文字数とバイト数がずれるため、このオフセット計算を誤ると、他アプリで貼り付けたときに先頭や末尾が欠けます。自前でHTML Formatを生成する場合は、UTF-8にエンコードした後のバイト位置でヘッダーを埋める必要があります。5

HTML Formatのヘッダーとオフセットの関係ヘッダーのStartHTMLとEndHTMLがHTML全体を、StartFragmentとEndFragmentがユーザーが選択した断片を、いずれもデータ先頭からのバイト位置で指す。UTF-8では文字数とバイト数がずれるため、エンコード後に実測したバイト位置で埋めるヘッダー(Version / StartHTML / EndHTML / StartFragment / EndFragment)HTML全体(StartHTML〜EndHTML)選択された断片(StartFragment〜EndFragment)各オフセット = データ先頭からのバイト位置(UTF-8エンコード後に実測して書き戻す)

このほか表形式データにはCSV(.NETのDataFormats.CommaSeparatedValue)もよく使われます。Excelとの相互運用では、HTML Format(書式付き)とCSV(値のみ)とCF_UNICODETEXT(タブ区切り)を併せて提供すると、貼り付け先を選びません。

4. 貼り付ける側の作法 ── 形式の優先順位と検証

4.1. リッチな形式から順に探す

クリップボード上の形式は、コピー側が置いた順(=表現力の高い順)に並んでいます。貼り付け側の基本は、自分が扱える形式のうち最も情報量の多いものから順に探すことです。Win32ではEnumClipboardFormatsで列挙して最初に認識できた形式を使うか、GetPriorityClipboardFormatに自分の優先順リストを渡して選びます。2

.NETなら次のような分岐になります。

// 表の貼り付け: リッチ→プレーンの順に探す
var data = Clipboard.GetDataObject();
if (data is null) return;

// 形式をうたっていても実体がstringとは限らない。型まで確認できたときだけ
// この枝を使い、ダメなら次の候補へ落とす
if (data.GetDataPresent(DataFormats.Html)
    && data.GetData(DataFormats.Html) is string html)
{
    // HTML Formatヘッダーを検証してから表として取り込む
}
else if (data.GetDataPresent(DataFormats.CommaSeparatedValue))
{
    // CSVとして取り込む
}
else if (data.GetDataPresent(DataFormats.UnicodeText))
{
    // タブ区切りテキストとして取り込む
}

「Excelの表を貼ると崩れる」という冒頭の相談への回答がこれです。プレーンテキストしか読まないアプリに表の構造は渡りません。どの形式まで受けるかは、貼り付け側の設計判断です。

リッチから順に探す貼り付けの分岐HTML Formatがあり実体も文字列なら表として取り込み、なければCSV、それもなければタブ区切りのテキストへと、表現力の高い形式から順に落としていく。どの候補もなければ受け入れないはいいいえはいいいえはいいいえ貼り付け開始HTML Formatがあり実体もstring?ヘッダーを検証して表として取り込むCSVがある?CSVとして取り込むUnicodeTextがある?タブ区切りテキストとして取り込む受け入れない

4.2. 貼り付けデータは外部入力である

見落とされがちですが、クリップボードの中身はどのアプリが置いたか分からない外部由来のデータです。MicrosoftもOLEクリップボードのドキュメントで「クリップボードのデータは信頼できない。アプリで使う前に注意深くパースせよ」と警告しています。7

  • HTML Formatのヘッダーオフセットが範囲外を指していないか検証する(壊れたヘッダーを吐くアプリは実在します)。
  • 数値・日付・コードとして取り込む値は、画面入力と同じバリデーションを通す。
  • 巨大なデータへの防御を入れる。数百MBの画像や数百万行のテキストが貼られても、UIをブロックせず、上限を超えたら断る設計にする。注意点として、.NETのGetDataは呼んだ時点で(遅延レンダリングも起動して)データ全体をマネージド文字列まで実体化させるため、サイズ確認をGetDataの後に置いても防御になりません。Win32でGetClipboardDataが返すHGLOBALをGlobalSizeで確認すれば「巨大データをマネージド文字列への変換・解析に進めない」段階の防御はできますが、遅延レンダリング形式ではGetClipboardData自体がレンダリングを起動するため、コピー元での実体化そのものまでは防げません。UIを固めないためには取得処理をUIスレッドの外へ出します(その場合も.NETのClipboardはSTA必須なので、Task.Runのスレッドプール(MTA)ではなく、STAに設定した専用スレッドで行います ── 5.1節)。

「外部から入ってくる値は、経路がなんであれ検証してから使う」という考え方は「QRコードの読み取り値をそのまま使ってはいけない」で整理したものと同じです。貼り付けはユーザー操作なので安全、という思い込みが事故のもとになります。

貼り付けデータを検証してから使う流れクリップボードから取り出すデータは、形式の存在確認、実体の型の確認、サイズ上限、内容の検証を順に通し、どこかで不合格なら断るか次の候補形式へ落とす型が違う大きすぎる不正形式の存在を確認(GetDataPresent)実体の型を確認(is string / string[])サイズ上限を確認内容を検証(ヘッダー・パス・値)取り込む断る / 次の候補形式へ

5. コピーする側の作法 ── 複数形式の同時提供と遅延レンダリング

5.1. 複数形式を同時に置く

コピー側の作法は4.1の裏返しで、リッチな形式とプレーンな形式を同時に提供することです。WinForms/WPFのDataObjectを使えば数行で書けます。15

// WinForms(System.Windows.Forms)。WPFもSystem.WindowsのDataObject/Clipboardで同型
var data = new DataObject();
data.SetData(DataFormats.Html, htmlFormatText);       // ヘッダー付きHTML Format文字列
data.SetData(DataFormats.CommaSeparatedValue, csv);   // CSV
data.SetData(DataFormats.UnicodeText, plainText);     // プレーンテキスト
Clipboard.SetDataObject(data, copy: true);            // copy:true=アプリ終了後も残す

2点補足します。第一に、.NETのClipboardクラスはSTAスレッドからしか使えません15 WinForms/WPFのUIスレッドは[STAThread]でSTAになっているので通常は問題ありませんが、バックグラウンドスレッドから触ろうとすると失敗します(STA/MTAの基礎は「COM STA/MTA の基礎知識」を参照)。第二に、copy: trueの意味は次項の遅延レンダリングと関係します。

5.2. 遅延レンダリング ── 「閉じたら貼れない」の正体

大きなデータを多数の形式で毎回作るのは無駄なので、クリップボードには遅延レンダリング(delayed rendering)という仕組みがあります。SetClipboardDataのデータハンドルにNULLを渡すと、データ本体の代わりに「要求されたら作る」という約束だけが登録され、誰かがその形式を要求した時点でコピー元にWM_RENDERFORMATが届き、そこで初めてデータを生成します。2

この仕組みの帰結が、冒頭の「コピー元アプリを閉じたら貼れなくなった」です。コピー元は終了前にWM_RENDERALLFORMATSを受け取り、未レンダリングの全形式を実体化する責務がありますが、これを怠って終了すると、その形式は失われます。2

遅延レンダリングの流れと「閉じたら貼れない」コピー元はNULLハンドルで約束だけを置き、要求が来たらWM_RENDERFORMATで実体化する。終了時はWM_RENDERALLFORMATSで全形式を実体化する責務があり、怠るとその形式は失われるWM_RENDERALLFORMATSで実体化実体化を怠るコピー元: SetClipboardData(format, NULL)で「約束」だけ登録貼り付け側がその形式を要求WM_RENDERFORMAT → その場でデータ生成コピー元が終了しようとする終了後も貼り付け可能その形式は失われる(「閉じたら貼れない」)

OLEクリップボード(IDataObjectをOleSetClipboardで置く方式)では、この関係がより明確です。クリップボードが保持するのはデータオブジェクトへのポインターだけで、アプリ終了時にOleFlushClipboardを呼ぶとデータがクリップボード上に実体化され、終了後も貼り付け可能になります6 .NETのClipboard.SetDataObject(data, copy: true)は、この「終了後も残す」動きを指定するものです。

Excelで大きな範囲をコピーして終了しようとすると「クリップボードに大きな情報があります。保持しますか?」と聞かれるのは、まさにこの確定処理(フラッシュ)を実行するかどうかの確認です。自作アプリで遅延レンダリングを使うなら、終了時の確定処理までがワンセットだと覚えてください。なお遅延レンダリングは性能最適化であり、レンダリング要求はメッセージ処理中に同期的に走るため、生成に時間がかかるデータではUIが固まるというトレードオフもあります。2

6. クリップボード監視の作法 ── リスナー・リトライ・履歴除外

6.1. AddClipboardFormatListenerを使う

「バーコードリーダーの値や基幹システムからのコピーを検知して自動で取り込みたい」といった要件では、クリップボードの変更監視が必要になります。方法は歴史的に3つありますが、現在の正解は1つです。8

方法 評価
タイマーで定期的に読む(ポーリング) 無駄が多く検知漏れもある。使わない
SetClipboardViewer(ビューアーチェーン) チェーン内の1アプリの不具合が全体を壊す。後方互換のためだけに残存
AddClipboardFormatListener 推奨。登録したウィンドウにWM_CLIPBOARDUPDATEが届く
クリップボード監視の流れハンドル作成時にAddClipboardFormatListenerで登録すると、どのアプリがコピーしてもWM_CLIPBOARDUPDATEが届く。読み取りはリトライつきで行い、ハンドル破棄時にRemoveClipboardFormatListenerで対称に解除する対称に解除OnHandleCreated: AddClipboardFormatListener待機どのアプリかがコピーWM_CLIPBOARDUPDATEが届くリトライつきで読み取り(6.2節)OnHandleDestroyed: RemoveClipboardFormatListener
// WinFormsでの最小実装
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)
    {
        // ハンドルの破棄・再作成に合わせて対称に解除する
        RemoveClipboardFormatListener(Handle);
        base.OnHandleDestroyed(e);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_CLIPBOARDUPDATE)
        {
            // ここでClipboard.GetDataObject()を読み、必要な形式なら取り込む
        }
        base.WndProc(ref m);
    }
}

6.2. 開けないときはリトライする

クリップボードを開けるのは一度に1つのウィンドウだけで、他のプロセスが開いている間、OpenClipboardは失敗します。2 WM_CLIPBOARDUPDATEを受けた直後は、まさにコピー元や他の監視アプリが操作している最中であることが多く、読み取りが一時的に失敗するのは正常事象です。短い待機(数十ミリ秒)を挟んだ数回のリトライを必ず入れてください。なお.NETのClipboardクラスでリトライ回数と間隔を指定できるオーバーロードは、書き込み側のSetDataObjectだけです。読み取り側(GetDataObject等)には無いため、WinFormsならExternalException、WPFならCOMExceptionをcatchして待機・再試行するコードを自分で書きます。

クリップボード読み取りリトライの流れクリップボードを開けるのは一度に1つのウィンドウだけなので、変更通知直後の読み取りは他プロセスと競合して失敗し得る。例外を受けたら数十ミリ秒待って再試行し、上限に達したら今回は諦めて次の更新で拾う成功失敗(他ウィンドウが使用中)数回まで再試行上限到達WM_CLIPBOARDUPDATE読み取りを試行取り込みへ(4章の検証)数十ミリ秒待機今回は諦める(次の更新で拾う)

6.3. 履歴・同期に載せない ── 機密を扱うコピー機能の配慮

Windowsにはクリップボード履歴(Win+V)とデバイス間同期(クラウドクリップボード)があり、アプリが置いたデータは既定でその対象になります。パスワードや口座番号のような機密をコピー機能に載せるアプリは、履歴・同期から除外するための登録形式を一緒に置きます。1

  • ExcludeClipboardContentFromMonitorProcessing: これを置くと、そのコピー内容全体が履歴にも同期にも含まれません。
  • CanIncludeInClipboardHistory(DWORD 0): 履歴のみ抑止。
  • CanUploadToCloudClipboard(DWORD 0): デバイス間同期のみ抑止。

パスワードマネージャーがコピーしたパスワードをWin+Vに残さないのは、この仕組みによるものです。名前をRegisterClipboardFormatに渡して形式IDを取得し、通常のデータと並べてセットするだけなので、機密を扱う業務アプリでは実装しておく価値があります。

7. 情シス視点のクリップボード ── 履歴・クラウド同期・RDPの制御

開発の話から少し離れて、管理者視点の論点を整理します。クリップボード履歴は直近のコピー内容を蓄積し、クラウドクリップボードは同じMicrosoftアカウント/Microsoft Entraアカウントでサインインしたデバイス間でコピー内容を同期します。10 便利な半面、基幹システムからコピーした個人情報が履歴に蓄積される、業務PCでコピーした内容が私物PCに同期される、といった情報の残留・越境が起きます。

組織で制御するポリシーは次の2つです。

制御対象 GPO(コンピューターの構成 > 管理用テンプレート > システム > OSポリシー) Policy CSP(Intune) 既定
クリップボード履歴 クリップボードの履歴を許可する Experience/AllowClipboardHistory 許可
デバイス間同期 デバイス間でのクリップボードの同期を許可する Privacy/AllowCrossDeviceClipboard 許可

いずれもWindows 10 バージョン1809以降で利用でき、無効化すると設定アプリの該当項目がグレーアウトし、ポリシーは即時反映されます。910

もう1つの定番がRDP(リモートデスクトップ)のクリップボードリダイレクトです。既定ではローカルPCとリモートセッションの間でコピペが通るため、サーバー上の機密の持ち出し経路になり得ます。「クリップボードのリダイレクトを許可しない」ポリシー(レジストリ値fDisableClip)で双方向とも遮断できます。11 近年のWindows Server/Windows 11には、サーバーからクライアントへの方向だけをテキストのみに制限するといった、より細かい制御を行うポリシーも追加されています。全面禁止か段階制限かは、運用とセキュリティのバランスで決めます。

クリップボードの内容が広がる経路と制御ポイントコピーした内容は既定で履歴とクラウド同期の対象になり、RDPではリダイレクトで別セッションへ渡る。それぞれポリシーで制御でき、アプリ側は除外形式で履歴と同期から外せるクリップボード履歴(Win+V)クラウド同期 → 別デバイスRDPリダイレクト → 別セッション制御: AllowClipboardHistory制御: AllowCrossDeviceClipboard制御: fDisableClipアプリ側: ExcludeClipboardContentFromMonitorProcessing等で除外(6.3節)

8. ドラッグ&ドロップの正体はCOM ── IDataObject+IDropSource+IDropTarget

8.1. クリップボードと同じデータ、違う運び方

OLEドラッグ&ドロップは、次の三者で動きます。12

役割 実装者 仕事
IDataObject ドラッグ元 運ばれるデータ本体。クリップボードと同じ複数形式のデータオブジェクト
IDropSource ドラッグ元 ドラッグ継続/中止の判断とカーソルフィードバック
IDropTarget ドロップ先 DragEnter/DragOver/DragLeave/Dropで受け入れ可否の表明と受け取り

ドラッグ元がDoDragDropを呼ぶとドラッグのループが始まり、マウスがドロップ先ウィンドウに入るとそのIDropTargetに通知が届き、ドロップ時にIDataObjectが渡されます。公式ドキュメントも「D&Dはクリップボードのコピー&ペーストとまったく同じ機能を提供する。コピペを実装済みのアプリなら追加はわずか」と述べています。12 2〜5章で作った複数形式のDataObjectが、そのままD&Dの荷物になるということです。

OLEドラッグ&ドロップの流れドラッグ元がIDataObjectを荷物にDoDragDropを呼んでドラッグのループが始まり、ドロップ先のIDropTargetがDragEnterとDragOverで受け入れ可否を表明し、DropでIDataObjectから形式を選んで取り出すDoDragDropマウスがウィンドウに入るボタンを離すドラッグ元: IDataObject + IDropSourceドラッグのループIDropTarget.DragEnter/DragOver(Effectを毎回表明)IDropTarget.DropIDataObjectから形式を選んで取り出す

8.2. OleInitialize(STA)が必須

ドロップ先になるウィンドウはRegisterDragDropで登録しますが、ここに古典的な落とし穴があります。COMの初期化をCoInitialize/CoInitializeExで行っているとRegisterDragDropは必ずE_OUTOFMEMORYで失敗し、OleInitializeで初期化しなければなりません13 OleInitializeはSTAとしてCOMを初期化するもので、D&DがウィンドウとメッセージポンプというSTAの世界に根差した機能だからです。呼び出しスレッドがメッセージポンプを回していることも必須で、怠るとドラッグ中の他アプリがハングします。13 このあたりの背景は「COM STA/MTA の基礎知識」で扱ったスレッドモデルの話そのものです。

WinForms/WPFアプリでは、フレームワークがOLEの初期化とインターフェース実装を肩代わりしてくれるため、開発者はイベントを書くだけで済みます。

// WinForms: ファイルのドロップを受け取る
listView1.AllowDrop = true;
listView1.DragEnter += (s, e) =>
{
    // ソース側がCopyを許可していることも確認する(Move/Linkしか許さないソースもある)
    e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop)
            && (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy
        ? DragDropEffects.Copy      // 受け入れ可: コピーとして受ける
        : DragDropEffects.None;     // 受け入れ不可
};
listView1.DragDrop += (s, e) =>
{
    // ドラッグデータも外部入力。FileDropをうたっていても実体がnullや
    // 別の型のことがあり、取得自体が失敗することもある
    object data;
    try { data = e.Data.GetData(DataFormats.FileDrop); }
    catch (COMException) { return; }
    if (data is not string[] paths) return;
    foreach (var path in paths)
    {
        // パスを検証してから取り込む(9.3節)
    }
};

WPFでも構図は同じで、要素のAllowDrop="True"DragOver/Dropイベントで受け、e.Data.GetData(DataFormats.FileDrop)でパス配列を取り出します。DragEnter/DragOverで受け入れ可否(Effect)を毎回表明するのがIDropTargetの流儀で、ここを省くとカーソルが「禁止」のまま変わらない、という不具合になります。

9. D&Dの落とし穴 ── 昇格・Move・パスの検証

9.1. 管理者昇格したアプリにはドロップできない

「管理者として実行」したアプリに、エクスプローラーからファイルをドロップしても反応しない ── これは実装ミスではなくOSの仕様です。UIPI(User Interface Privilege Isolation)が、低い整合性レベルのプロセスから高い整合性レベルのウィンドウへのメッセージを既定で遮断するためで、通常権限(中整合性)のエクスプローラーから昇格アプリへはドロップの通知が届きません。14

昇格アプリへのドロップがUIPIに遮断される仕組み中整合性のエクスプローラーから高整合性の昇格アプリへのドロップ通知はUIPIが既定で遮断するため届かない。UIを通常権限に保ち特権処理を分離すればドロップは届くドロップ通知遮断(既定)通過特権処理だけ依頼エクスプローラー(中整合性)UIPI昇格アプリ(高整合性): 無反応通常権限のUI: ドロップが届く昇格が必要な処理を分離した別プロセス

ChangeWindowMessageFilterExでWM_DROPFILESなど特定のメッセージを個別に許可する回避策が知られていますが14、これで通るのは旧式(WM_DROPFILES)のドロップ通知であり、OLE D&D全体が解決するわけではありません。実務の指針は明快で、アプリを常時昇格で動かす設計をやめることです。昇格が必要な処理だけを別プロセスに分離すれば、UI本体は通常権限のままD&Dを受けられます(分離の設計は「Windowsアプリの管理者権限とブローカープロセス」で詳述しています)。

9.2. DragDropEffectsの意味 ── Moveは「元が消える」契約

DragDropEffectsのCopy/Move/Linkは飾りではなく、ドラッグ元とドロップ先の間の契約です。ドラッグ元はDoDragDropで許可する効果の集合を宣言し、ドロップ先が実際の効果を選び、Moveが成立するとドラッグ元がデータ(ファイル)を削除するのが規約です。受け側が深く考えずMoveを返すと「ドロップしたら元のファイルが消えた」という事故になります。業務アプリの取り込み用途では、受け側はCopyを明示するのが安全側の既定です。

DragDropEffectsの契約 ── Moveでは元が消えるドラッグ元がDoDragDropで許可する効果の集合を宣言し、ドロップ先が実際の効果を選ぶ。Moveが成立するとドラッグ元がファイルを削除する規約のため、取り込み用途の受け側はCopyを明示するのが安全CopyMoveドラッグ元: 許可する効果を宣言(Copy | Move | Link)ドロップ先: 実際の効果を選ぶ元ファイルは残る(取り込み用途の安全側)ドラッグ元がファイルを削除 ──「元が消えた」事故のもと

9.3. ドロップされたパスの検証

CF_HDROP/FileDropで渡るのはパスだけです(3.2節)。取り込む前に、貼り付けと同じく外部入力としての検証を通します。

  • ファイルかフォルダーか: フォルダーごとドロップされるケースを仕様として決めておく(再帰で取り込むのか、断るのか)。
  • OneDriveのプレースホルダー: パスは存在してもファイル本体がローカルにない「オンデマンドファイル」の場合、開いた瞬間にダウンロードが走り、オフラインなら失敗します。挙動と対策は「OneDrive「ファイル オンデマンド」と業務アプリ」を参照してください。
  • 長いパス・特殊なパス: MAX_PATH超のパス、ネットワーク(UNC)パス、リムーバブルメディア上のパスは、後続処理が対応できるかを確認してから受け入れます。
  • 件数と合計サイズ: 数千ファイルのドロップでUIが固まらないよう、取り込みは非同期化し、上限や進捗表示を設けます。

10. まとめ

  • クリップボードは同じデスクトップ(ウィンドウステーション)内で共有される1つの領域に、同じ内容を複数の形式で同時に置く仕組みです。貼り付け先が形式を選ぶため、同じコピーでも結果が変わります。
  • テキストはCF_UNICODETEXT、ファイルはCF_HDROP、書式付きテキストは登録形式のHTML Format(バイトオフセットヘッダー+UTF-8)です。
  • 貼り付ける側はリッチ→プレーンの順に形式を探し、中身は外部入力として検証します。コピーする側は複数形式を同時に提供し、遅延レンダリングを使うなら終了時の確定(WM_RENDERALLFORMATS/OleFlushClipboard)までを実装します。
  • 監視はAddClipboardFormatListener+WM_CLIPBOARDUPDATE。OpenClipboardの競合にはリトライで備え、機密はExcludeClipboardContentFromMonitorProcessing等で履歴・同期から除外します。
  • 情シスはクリップボード履歴・クラウド同期・RDPリダイレクトをGPO/Intuneで制御できます。既定はすべて許可なので、機密を扱う環境では意識的に判断してください。
  • D&Dの正体はCOMで、クリップボードと同じIDataObjectをIDropSource/IDropTargetが受け渡します。RegisterDragDropはOleInitialize(STA)が必須です。
  • 昇格アプリへのドロップはUIPIで遮断されます。DragDropEffectsのMoveは「元が消える」契約、ドロップされたパスは検証してから取り込みます。

コピペとD&Dは、ユーザーにとっては空気のような機能です。だからこそ、「貼れない」「崩れる」「消えた」が起きたときの体験の悪化は大きく、逆に複数形式の提供とドロップ対応が行き届いたアプリは、それだけで日々の操作が滑らかになります。改修の優先順位を考える際の判断材料になれば幸いです。

関連記事

関連する相談領域

合同会社小村ソフトでは、業務アプリへのコピー&ペースト・ドラッグ&ドロップ対応の設計と実装(複数形式の提供、Excel連携、ファイルドロップ取り込み)、「貼り付けると崩れる」「コピーが消える」といった不具合の原因調査、クリップボード監視を使った入力自動化、機密情報の履歴・同期対策の実装を扱っています。COMやOLEの低レイヤーが絡む案件も、現象の切り分けからで構いません。

参考リンク

  1. Microsoft Learn, Clipboard Formats. ウィンドウが同じ情報を複数のクリップボード形式で置けること、RegisterClipboardFormatによる登録形式(同名の登録は同じ値を返しアプリ間で共有できる)、合成形式、ExcludeClipboardContentFromMonitorProcessing・CanIncludeInClipboardHistory・CanUploadToCloudClipboardによるクリップボード履歴/クラウド同期からの除外について。  2 3 4 5

  2. Microsoft Learn, Clipboard Operations. クリップボードを開けるのは一度に1ウィンドウだけであること、コピー時に表現力の高い形式から順に置くこと、貼り付け時のEnumClipboardFormats/GetPriorityClipboardFormatによる形式選択、SetClipboardDataにNULLを渡す遅延レンダリングと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_LOCALEに関連付いたコードページを使ってCF_TEXTとCF_UNICODETEXTがシステムにより暗黙変換されることについて。  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. クリップボード監視の3方式(ビューアーウィンドウ・シーケンス番号・フォーマットリスナー)の比較、新規プログラムはAddClipboardFormatListenerによるリスナーを使うべきこと、ビューアーチェーンがチェーン維持の不備に脆弱なこと、シーケンス番号をポーリングに使うべきでないことについて。  2

  9. Microsoft Learn, Policy CSP - Experience. Experience/AllowClipboardHistoryポリシーによるクリップボード履歴の許可/禁止、Windows 10 バージョン1809以降で利用可能なこと、既定が許可であること、GPOでは「システム > OSポリシー」配下にマップされ変更が即時反映されることについて。  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の登録方法、COMをCoInitialize/CoInitializeExで初期化した場合は常にE_OUTOFMEMORYで失敗し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

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Excelからコピーした表をアプリに貼り付けると書式が崩れるのはなぜですか?
クリップボードには「1つのデータ」ではなく、同じ内容が複数の形式(アプリ独自形式、HTML Format、CSV、Unicodeテキストなど)で同時に置かれており、貼り付け先のアプリが自分の理解できる形式を選んで取り出すからです。書式が崩れる場合、貼り付け先がプレーンテキスト(CF_UNICODETEXT)しか読んでいないのが典型です。表の構造まで受け取りたければ、貼り付け側でHTML FormatやCSV形式を優先して取り出す実装にします。逆に自アプリからのコピーで他アプリに正しく貼らせたい場合は、コピー時にリッチな形式とプレーンな形式を同時に提供します。
コピー元のアプリを閉じると貼り付けられなくなるのはなぜですか?
コピー元が遅延レンダリング(delayed rendering)を使っているためです。大きなデータを扱うアプリは、コピー時にはデータ本体を置かず「要求されたら作る」という約束だけをクリップボードに登録します。この状態でコピー元が、終了時のWM_RENDERALLFORMATSに応答してデータを確定しないまま終了すると、未レンダリングの形式は失われます。OLEクリップボード(IDataObject)を使うアプリなら、終了時にOleFlushClipboardを呼んでデータを実体化しておくことで、終了後も貼り付け可能な状態を維持できます。
自作アプリからクリップボードの変更を監視するにはどうすればよいですか?
AddClipboardFormatListenerで自分のウィンドウをリスナーとして登録し、内容が変わるたびに届くWM_CLIPBOARDUPDATEメッセージを処理するのが現在の推奨方法です。タイマーで定期的に内容を読むポーリングは無駄が多く、SetClipboardViewerによる旧来のビューアーチェーンは、チェーン内の1つのアプリの不具合が全体を壊すため後方互換のためだけに残されています。なお読み取り時のOpenClipboardは他プロセスと競合して失敗し得るので、短い待機を挟んだリトライを実装しておくと安定します。
パスワードなどの機密情報をクリップボード履歴(Win+V)に残さない方法はありますか?
アプリ側とポリシー側の2つの手段があります。アプリ側では、コピー時にExcludeClipboardContentFromMonitorProcessingという登録形式を一緒に置くと、その内容は履歴にもデバイス間同期にも含まれません。CanIncludeInClipboardHistory(履歴のみ)やCanUploadToCloudClipboard(同期のみ)で個別制御もできます。パスワードマネージャーが使っているのはこの仕組みです。組織全体で止めたい場合は、グループポリシーやIntune(Policy CSP)のAllowClipboardHistory・AllowCrossDeviceClipboardで履歴・クラウド同期そのものを無効化できます。
管理者として実行したアプリにファイルをドラッグ&ドロップできないのはなぜですか?
UIPI(User Interface Privilege Isolation)というセキュリティ機構が、低い整合性レベルのプロセスから高い整合性レベルのウィンドウへのメッセージ送信を遮断するためです。エクスプローラーは通常権限(中整合性)で動いているので、昇格したアプリのウィンドウにはドラッグ&ドロップの通知が届きません。ChangeWindowMessageFilterExでWM_DROPFILESなどを個別に許可する回避策も知られていますが、旧式のドロップ通知に限られます。根本的には、アプリを常時昇格で動かす設計をやめ、昇格が必要な処理だけを別プロセスに分離するのが正攻法です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る