All Articles

Windows のクリップボード実装に関するメモ書き

今回は Windows のクリップボード操作やしくみ、データの参照について、学習したことをサンプルプログラムとともにまとめます。

もくじ

Windows のクリップボードについて

クリップボード自体は、アプリケーションがデータを転送できるようにするための仕組みです。

クリップボード機能を使用することで、ユーザーは異なるアプリケーション間でテキストやファイル、画像など様々なデータをやり取りすることができます。

参考:クリップボードについて - Win32 apps | Microsoft Learn

クリップボードの形式

Windows のクリップボードにはテキストはもちろん、HTML やリッチテキスト、様々な形式のオブジェクトを保存できます。

これにより、例えばブラウザやエディタなどから他のアプリケーションにテキストをコピーしようとした場合に、単なるテキストだけでなく文字の装飾や背景色など様々な情報がコピーされるといった状況が発生します。

多くの場合に使用されるデータの種類は Windows の標準のクリップボードの形式に含まれていますが、ユーザーが新しいクリップボード形式を登録することもできます。

また、アプリケーションによりプライベート形式というものも登録することができるようです。

参考:クリップボードの形式 - Win32 apps | Microsoft Learn

システムに事前定義されたクリップボード形式には様々な種類があり、例えばコピーしたテキストは自動的に CF_UNICODETEXTCF_TEXT などに保存されるようです。

参考:標準クリップボード形式 (Winuser.h) - Win32 apps | Microsoft Learn

クリップボードの操作

クリップボードの操作を実装するために使用可能な方法はいくつかありそうですが、どの方法を使用する場合でも最終的には OpenClipboard 関数 (winuser.h) を使用してクリップボードを開くことがスタートになるようです。

また、システムで同時にクリップボードを開くことができるアプリケーションは 1 つのみであるため、特定のアプリケーションが OpenClipboard 関数を使用してクリップボードを開いている場合には他のアプリケーションがクリップボードを開くことができなくなるようです。

参考:クリップボード操作 - Win32 apps | Microsoft Learn

実際に、テストとして以下のプログラムを実行してみると、OpenClipboard 関数によりクリップボードを取得した後のスリープ時間中には、ブラウザなどの他のアプリケーションでテキストのコピーやペースト操作が動作しなくなりました。

#include <windows.h>
#include <iostream>
#include <string>

void OpenClipboardWithSleep() {

    if (!OpenClipboard(NULL)) {
        std::cerr << "Failed to open clipboard." << std::endl;
        return;
    }

    // Sleep for demonstration purposes (not required in production)
    Sleep(10000);

    CloseClipboard();

}

int wmain() {

    OpenClipboardWithSleep();
    return 0;
}

なお、クリップボード操作に関するサンプルコードは以下にまとめられています。

参考:クリップボードの使用 - Win32 apps | Microsoft Learn

クリップボードの所有権

クリップボードに情報をコピーする際にはまず、EmptyClipboard 関数 (winuser.h) を呼び出してクリップボード内のデータを空にしてハンドルを解放した後に、そのクリップボードの所有権を現在クリップボードを開いているウィンドウに割り当てます。

クリップボードの所有者は GetClipboardOwner 関数 (winuser.h) により調べることができます。

また、クリップボードには遅延レンダリングというパフォーマンス向上のための仕組みが存在しており、SetClipboardData 関数 (winuser.h) でクリップボードにデータを保存する際に hMem パラメーターに NULL を指定することで利用することができます。

遅延レンダリングを使用すると、コピー時ではなく貼り付けなどの要求時に、要求された形式のデータがクリップボードの所有者からコピーされる動作となり、不要なレンダリングによるパフォーマンス影響を回避できます。

なお、遅延レンダリングは大きなデータなどをコピーする際のパフォーマンスが改善される反面、ペースト時にアプリケーションの応答が遅延することや、アプリケーションが終了しているとデータをコピーできないなどのデメリットも存在するようです。

参考:クリップボード操作 - Win32 apps | Microsoft Learn

参考:スペック次第で性能が変わるWindowsのクリップボード | どすらぼ

クリップボード操作の実装

Windows クリップボードの学習のため、いくつかのコードを作成して動作を確認しました。

Win32 API による Read 操作

以下は、GetClipboardData 関数 (winuser.h) でクリップボード内のデータを読み出した結果を HEX Dump 化したものです。

image-20260104091655978

image-20260104091645000

以下が、GetClipboardData 関数によるデータ読み出しを行う関数の実装です。

void ReadAndDumpClipboard() {

    if (!OpenClipboard(NULL)) {
        std::cerr << "Failed to open clipboard." << std::endl;
        return;
    }

    std::cout << "=== Clipboard Content Dump ===" << std::endl;

    UINT format = 0;
    int count = 0;

	// Enumerate all clipboard formats
    while ((format = EnumClipboardFormats(format)) != 0) {
        count++;
        std::wstring formatName = GetFormatNameString(format);

        std::wcout << L"[" << count << L"] Format ID: " << std::dec << format
            << L" | Name: " << formatName << std::endl;

		// Get clipboard data handle
        HANDLE hData = GetClipboardData(format);
        if (hData == NULL) {
            std::cout << "    [Error] GetClipboardData returned NULL." << std::endl;
            continue;
        }

		// If we can lock the data, we assume it's a memory block
        void* pData = GlobalLock(hData);
        if (pData) {
            size_t size = GlobalSize(hData);
            std::cout << "    Type: Memory Block | Size: " << std::dec << size << " bytes" << std::endl << std::endl;

            PrintHexDump(pData, size);

            GlobalUnlock(hData);
        }
        else {
			// Fallback for non-lockable formats
            std::cout << "    Type: GDI Handle or Non-Lockable Object (Cannot dump raw bytes safely)" << std::endl << std::endl;
        }
    }

    if (count == 0) {
        std::cout << "Clipboard is empty." << std::endl;
    }

    CloseClipboard();

}

GetClipboardData 関数は、引数として受け取ったクリップボード形式の値と対応するクリップボードオブジェクトへのハンドルを返す関数です。

HANDLE GetClipboardData(
  [in] UINT uFormat
);

クリップボードで現在使用できるクリップボード形式のリストについては、EnumClipboardFormats 関数 (winuser.h) の呼び出しごとに取得することができるため、以下のループで順にクリップボードデータを参照しています。(利用可能なクリップボードデータがそれ以上存在しない場合、関数の戻り値は 0 になります)

// Enumerate all clipboard formats
while ((format = EnumClipboardFormats(format)) != 0) { }

取得したクリップボードオブジェクトのハンドル内のデータは クリップボードから情報を貼り付ける のサンプルコードのように、GlobalLock 関数 (winbase.h) によりメモリブロックをロックした上で読み取りを行います。

// If we can lock the data, we assume it's a memory block
void* pData = GlobalLock(hData);
if (pData) {
    size_t size = GlobalSize(hData);
    std::cout << "    Type: Memory Block | Size: " << std::dec << size << " bytes" << std::endl << std::endl;

    PrintHexDump(pData, size);

    GlobalUnlock(hData);
}

SetClipboardData 関数 (winuser.h) によりクリップボードデータを保存する場合には、保存先のグローバルメモリは GMEM_MOVEABLE フラグを使用して移動可能メモリとして割り当てされていることが前提となるため、このメモリ領域にアクセスするために GlobalLock 関数を必要とします。

hMem パラメーターがメモリ オブジェクトを識別する場合、オブジェクトは GMEM_MOVEABLE フラグを持つ 関数を使用して割り当てられている必要があります。

参考:c++ - Windows API - Clipboard - GlobalLock - use or not to use? - Stack Overflow

参考:クリップボード

Win32 API による Write 操作

クリップボードへの情報の書き込み(コピー)操作についても、以下のサンプルコードと同様の処理を実装することで実現できます。

参考:情報をクリップボードにコピーする

// Using Win32 API to write and read clipboard text
bool WriteTextToClipboard(const std::wstring& text) {
    
    if (!OpenClipboard(NULL)) {
        std::cerr << "Failed to open clipboard." << std::endl;
        return false;
    }
    if (!EmptyClipboard()) {
        std::cerr << "Failed to empty clipboard." << std::endl;
        CloseClipboard();
        return false;
	}

	// Calculate size in bytes (including null terminator)
    size_t size = (text.length() + 1) * sizeof(wchar_t);

	// Need to allocate global memory for clipboard data(GMEM_MOVEABLE)
    HGLOBAL hGlob = GlobalAlloc(GMEM_MOVEABLE, size);
    if (!hGlob) {
        CloseClipboard();
        return false;
    }

	// Lock the memory and copy the text
    void* pMem = GlobalLock(hGlob);
    if (pMem) {

        memcpy(pMem, text.c_str(), size);
        GlobalUnlock(hGlob);

		// Set the clipboard data as CF_UNICODETEXT
        if (SetClipboardData(CF_UNICODETEXT, hGlob) == NULL) {
            GlobalFree(hGlob);
            std::cerr << "Failed to set clipboard data." << std::endl;
        }
        else {
            std::cout << "[Write] Successfully copied text to clipboard." << std::endl;
        }
    }

    CloseClipboard();
    return true;
}

上記のコードでは、まず引数を NULL として呼び出した OpenClipboard 関数でクリップボードを開いて現在のタスクに割り当てた後、EmptyClipboard 関数 (winuser.h) でクリップボードを空にした上でクリップボードの所有権を現在のウインドウに割り当てます。

さらに、前述の通り GMEM_MOVEABLE フラグを使用して GlobalAlloc 関数 (winbase.h) により移動可能メモリを確保し、GlobalLock -> memcpy -> GlobalUnlock によりクリップボードに保存したいデータを書き込みます。

最後に、SetClipboardData 関数 (winuser.h) で保存したいデータを書き込んだハンドルを CF_UNICODETEXT 形式のクリップボードとして配置します。

これで、以下のように CF_UNICODETEXT 形式のクリップボードに保存したテキストがワイド文字列として書き込まれたことを確認できます。

なお、Windows では特定のクリップボード形式は対応する他の形式のデータとしても自動的に保存されます。

参考:クリップボードの形式 - Win32 apps | Microsoft Learn

そのため、SetClipboardData(CF_UNICODETEXT, hGlob) にて明示的に CF_UNICODETEXT 形式のクリップボードのみにデータを保存した場合でも、 CF_UNICODETEXT の暗黙的な型変換対象である CF_TEXTCF_OEMTEXT に 1 バイト文字列に自動変換されたテキストが保存される動作となっているようです。

image-20260104180352020

OLE クリップボード

Windows では、ここまでに確認した Win32 API を直接使用する方法とは別に、OLE/COM モデルを使用する方法で実装することもできます。

COM ベースの OLE クリップボードを使用する場合、OleGetClipboard 関数 (ole2.h) で取得した IDataObject インターフェイス (System.Windows.Forms) オブジェクトをインターフェースとしてデータのやり取りが行われます。

OleGetClipboard 関数で取得したクリップボードのデータを受け取るための IDataObject インターフェースから様々な形式のクリップボードデータを読み出す場合、DATADIR_GET をパラメーターに渡した IDataObject::EnumFormatEtc (objidl.h) を使用して列挙したデータオブジェクトでサポートされている形式を指定して IDataObject.GetData メソッド (System.Windows) を呼び出します。

EnumFormatEtc メソッドで取得できる FORMATETC (objidl.h) は一般化されたクリップボード形式を抽象化しています。

また、OLE クリップボードではグローバルメモリ以外のストレージメディアなどもデータ転送に使用できるように拡張されているらしく、FORMATETC 構造体の TYMED 列挙型でメディアの種類を指しているようです。

例えば、この値が TYMED_HGLOBAL を指す場合にはストレージメディアとしてグローバルメモリハンドルが使用されていることを意味します。

参考:クリップボード: OLE クリップボード メカニズムの使用 | Microsoft Learn

参考:The OLE Clipboard

まとめ

Windows のクリップボードについてメモ書きしました。