EternalWindows
汎用データ転送 / データオブジェクトの実装

OLEはクリップボード内のデータをIDataObjectとして返すだけでなく、 IDataObjectからクリップボード内にデータを設定する方法も提供しています。 これには、OleSetClipboardを呼び出します。

HRESULT OleSetClipboard(
  LPDATAOBJECT pDataObj
);

pDataObjは、IDataObjectを実装したオブジェクトのアドレスを指定します。

pDataObjが有効なポインタである場合、OleSetClipboardは最初にオブジェクトのAddRefを呼び出します。 次に、オブジェクトのQueryInterfaceからIEnumFORMATETCを取得し、 IEnumFORMATETC::Nextを繰り返し呼び出すことによって、オブジェクトがサポートしているフォーマットを取得します。 この後に何が行われるかについてですが、 当初筆者は取得したフォーマットを基にIDataObject::GetDataが呼ばれ、 その取得したデータと共にSetClipboardDataが呼ばれると思っていました。 しかし、実際にはIDataObject::GetDataは直ちに呼ばれず、 SetClipboardDataはデータをNULLに指定して呼ばれることになっています。 このような方法は遅延レンダリングと呼ばれ、 実際にユーザーがGetClipboardDataを呼び出した際に、 データを動的に取得する狙いを持っています。

遅延レンダリングを採用している場合、GetClipboardDataは設定されているウインドウに対してWM_RENDERFORMATを送信します。 設定されているウインドウとは、OpenClipboardの呼び出しの際に指定したウインドウのことであり、 これはOleInitializeで作成される非表示のウインドウになります。 WM_RENDERFORMATを受け取ったOLEは、データをGetClipboardDataの呼び出し側に返さなければなりませんが、 このときにようやくIDataObject::GetDataが呼ばれることになります。 これはつまり、GetClipboardDataが呼ばれる際にはオブジェクトが必ず存在しなければならないということですから、 OleSetClipboardを呼び出したアプリケーションは、自分の都合で無闇に終了するわけにはいかないことになります。 もしそのようなことをした場合は、GetClipboardDataの呼び出し側がデータを取得できないことになってしまうからです。 この問題を顕著に感じる例として、Wordで表示される次のようなダイアログが挙げられます。

Wordで何らかの図形を作成してそれをコピーし、アプリケーションを終了しようとすると上記のダイアログが表示されます。 これは正に、Wordが終了したらコピーした図形を使用できなくなることを意味しているといえるでしょう。 ちなみに、自作のデータオブジェクトが現在もクリップボードに設定されているかは、 OleIsCurrentClipboardを呼び出すことで確認できるようになっています。 クリップボードに設定されたデータオブジェクトの参照カウントは、 何からのコピーを実行するかOleFlushClipboardの呼び出しで下がります。

今回のプログラムは、起動時に自作のデータオブジェクトをクリップボードに設定します。 データオブジェクトは、テキスト形式とビットマップ形式をサポートしているため、 メモ帳にはデータをテキスト形式でペーストすることができ、 ペイントにはデータをビットマップ形式でペーストすることができます。 表示されるウインドウを閉じると、ペーストすることができなくなる点に注意してください。

#include <windows.h>

class CDataObject : public IDataObject, public IEnumFORMATETC
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP GetData(FORMATETC *pformatetcIn, STGMEDIUM *pmedium);
	STDMETHODIMP GetDataHere(FORMATETC *pformatetc, STGMEDIUM *pmedium);
	STDMETHODIMP QueryGetData(FORMATETC *pformatetc);
	STDMETHODIMP GetCanonicalFormatEtc(FORMATETC *pformatectIn, FORMATETC *pformatetcOut);
	STDMETHODIMP SetData(FORMATETC *pformatetc, STGMEDIUM *pmedium, BOOL fRelease);
	STDMETHODIMP EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc);
	STDMETHODIMP DAdvise(FORMATETC *pformatetc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection);
	STDMETHODIMP DUnadvise(DWORD dwConnection);
	STDMETHODIMP EnumDAdvise(IEnumSTATDATA **ppenumAdvise);
	
	STDMETHODIMP Next(ULONG celt, FORMATETC *rgelt, ULONG *pceltFetched);
	STDMETHODIMP Skip(ULONG celt);
	STDMETHODIMP Reset(VOID);
	STDMETHODIMP Clone(IEnumFORMATETC **ppenum);
	
	CDataObject();
	~CDataObject();

private:
	LONG  m_cRef;
	ULONG m_uEnumCount;
	char  m_szText[256];
};

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static IDataObject *pDataObject = NULL;

	switch (uMsg) {

	case WM_CREATE:
		OleInitialize(NULL);
		pDataObject = new CDataObject();
		OleSetClipboard(pDataObject);
		return 0;

	case WM_DESTROY:
		if (pDataObject != NULL)
			pDataObject->Release();
		OleUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


// CDataObject


CDataObject::CDataObject()
{
	m_cRef = 1;
	m_uEnumCount = 0;
	lstrcpyA(m_szText, "sample");
}

CDataObject::~CDataObject()
{
}

STDMETHODIMP CDataObject::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IDataObject))
		*ppvObject = static_cast<IDataObject *>(this);
	else if (IsEqualIID(riid, IID_IEnumFORMATETC))
		*ppvObject = static_cast<IEnumFORMATETC *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

STDMETHODIMP_(ULONG) CDataObject::AddRef()
{
	return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CDataObject::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0) {
		delete this;
		return 0;
	}

	return m_cRef;
}

STDMETHODIMP CDataObject::GetData(FORMATETC *pformatetcIn, STGMEDIUM *pmedium)
{
	if (pformatetcIn->cfFormat == CF_TEXT) {
		HGLOBAL hglobal;
		char    *p;

		hglobal = GlobalAlloc(GHND, lstrlenA(m_szText) + 1);

		p = (char *)GlobalLock(hglobal);
		lstrcpyA(p, m_szText);
		GlobalUnlock(hglobal);

		pmedium->tymed = TYMED_HGLOBAL;
		pmedium->hGlobal = hglobal;
		pmedium->pUnkForRelease = NULL;
	}
	else if (pformatetcIn->cfFormat == CF_BITMAP) {
		HDC     hdcMem;
		HBITMAP hbmpMem, hbmpMemPrev;
		SIZE    size;
		RECT    rc;

		hdcMem = CreateCompatibleDC(NULL);
		GetTextExtentPoint32A(hdcMem, m_szText, lstrlenA(m_szText), &size);
		hbmpMem = CreateCompatibleBitmap(hdcMem, size.cx, size.cy);
		hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);

		SetRect(&rc, 0, 0, size.cx, size.cy);
		FillRect(hdcMem, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH));

		SetBkMode(hdcMem, TRANSPARENT);
		TextOutA(hdcMem, 0, 0, m_szText, lstrlenA(m_szText));
		
		pmedium->tymed = TYMED_GDI;
		pmedium->hBitmap = hbmpMem;
		pmedium->pUnkForRelease = NULL;
		
		SelectObject(hdcMem, hbmpMemPrev);
		DeleteDC(hdcMem);
	}
	else
		return E_FAIL;
	
	return S_OK;
}

STDMETHODIMP CDataObject::GetDataHere(FORMATETC *pformatetc, STGMEDIUM *pmedium)
{
	if (pformatetc->cfFormat == CF_TEXT) {
		ULONG uWritten;
		pmedium->pstm->Write(m_szText, lstrlenA(m_szText) + 1, &uWritten);
	}
	else
		return E_FAIL;

	return S_OK;
}

STDMETHODIMP CDataObject::QueryGetData(FORMATETC *pformatetc)
{
	if (pformatetc->cfFormat == CF_TEXT || pformatetc->cfFormat == CF_BITMAP)
		return S_OK;

	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::GetCanonicalFormatEtc(FORMATETC *pformatectIn, FORMATETC *pformatetcOut)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::SetData(FORMATETC *pformatetc, STGMEDIUM *pmedium, BOOL fRelease)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc)
{
	if (dwDirection == DATADIR_GET)
		return QueryInterface(IID_PPV_ARGS(ppenumFormatEtc));
	else
		return E_NOTIMPL;
}


STDMETHODIMP CDataObject::DAdvise(FORMATETC *pformatetc, DWORD advf, IAdviseSink *pAdvSink, DWORD *pdwConnection)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::DUnadvise(DWORD dwConnection)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::EnumDAdvise(IEnumSTATDATA **ppenumAdvise)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::Next(ULONG celt, FORMATETC *rgelt, ULONG *pceltFetched)
{
	FORMATETC formatetc[] = {
		{CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL | TYMED_ISTREAM},
		{CF_BITMAP, NULL, DVASPECT_CONTENT, -1, TYMED_GDI}
	};

	if (m_uEnumCount >= 2)
		return S_FALSE;
	
	*rgelt = formatetc[m_uEnumCount];

	if (pceltFetched != NULL)
		*pceltFetched = 1;
	
	m_uEnumCount++;

	return S_OK;
}

STDMETHODIMP CDataObject::Skip(ULONG celt)
{
	return E_NOTIMPL;
}

STDMETHODIMP CDataObject::Reset(VOID)
{
	m_uEnumCount = 0;

	return S_OK;
}

STDMETHODIMP CDataObject::Clone(IEnumFORMATETC **ppenum)
{
	return E_NOTIMPL;
}

OleSetClipboardには、IDataObjectを実装したオブジェクトのアドレスを指定しなければならないため、 そのためのクラスとしてCDataObjectを定義しています。 このクラスはIEnumFORMATETCも継承していますが、 これはオブジェクトがサポートするフォーマットをOLEが取得できるようにするためです。

STDMETHODIMP CDataObject::Next(ULONG celt, FORMATETC *rgelt, ULONG *pceltFetched)
{
	FORMATETC formatetc[] = {
		{CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL | TYMED_ISTREAM},
		{CF_BITMAP, NULL, DVASPECT_CONTENT, -1, TYMED_GDI}
	};

	if (m_uEnumCount >= 2)
		return S_FALSE;
	
	*rgelt = formatetc[m_uEnumCount];

	if (pceltFetched != NULL)
		*pceltFetched = 1;
	
	m_uEnumCount++;

	return S_OK;
}

Nextでは、オブジェクトがサポートするフォーマットを配列として定義し、 それらをNextが呼ばれる度に1つずつ返すことになります。 サポートするフォーマットはテキスト形式とビットマップ形式を予定しているため、 CF_TEXTとCF_BITMAPのFORMATETC構造体をそれぞれ定義しています。 m_uEnumCountは列挙したフォーマットの数を表しており、 これがサポートできるフォーマット数に到達した場合は、 列挙を終了するためにS_FALSEを返すようにします。 列挙が可能である場合は、m_uEnumCountをインデックスとしてフォーマットをrgeltに返し、 pceltFetchedに今回列挙したフォーマットの数を返します。

既に述べたように、OleSetClipboardは遅延レンダリングを採用しているため、 何からのアプリケーションがGetClipboardDataを呼び出した際に、 IDataObject::GetDataが呼ばれます。 ここでは、要求されたフォーマットに応じてデータを返します。

STDMETHODIMP CDataObject::GetData(FORMATETC *pformatetcIn, STGMEDIUM *pmedium)
{
	if (pformatetcIn->cfFormat == CF_TEXT) {
		HGLOBAL hglobal;
		char    *p;

		hglobal = GlobalAlloc(GHND, lstrlenA(m_szText) + 1);

		p = (char *)GlobalLock(hglobal);
		lstrcpyA(p, m_szText);
		GlobalUnlock(hglobal);

		pmedium->tymed = TYMED_HGLOBAL;
		pmedium->hGlobal = hglobal;
		pmedium->pUnkForRelease = NULL;
	}
	else if (pformatetcIn->cfFormat == CF_BITMAP) {
		HDC     hdcMem;
		HBITMAP hbmpMem, hbmpMemPrev;
		SIZE    size;
		RECT    rc;

		hdcMem = CreateCompatibleDC(NULL);
		GetTextExtentPoint32A(hdcMem, m_szText, lstrlenA(m_szText), &size);
		hbmpMem = CreateCompatibleBitmap(hdcMem, size.cx, size.cy);
		hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);

		SetRect(&rc, 0, 0, size.cx, size.cy);
		FillRect(hdcMem, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH));

		SetBkMode(hdcMem, TRANSPARENT);
		TextOutA(hdcMem, 0, 0, m_szText, lstrlenA(m_szText));
		
		pmedium->tymed = TYMED_GDI;
		pmedium->hBitmap = hbmpMem;
		pmedium->pUnkForRelease = NULL;
		
		SelectObject(hdcMem, hbmpMemPrev);
		DeleteDC(hdcMem);
	}
	else
		return E_FAIL;
	
	return S_OK;
}

オブジェクトがサポートできるフォーマットはテキスト形式とビットマップ形式であるため、 CF_TEXTの条件式とCF_BITMAPの条件式をそれぞれ用意することになります。 CF_TEXTではテキストの長さだけのメモリを確保し、そこにテキストをコピーすればよいでしょう。 pmediumには返すことになるデータを格納することになり、 データがグローバルメモリで識別されている場合は、 tymedにTYMED_HGLOBALを指定します。 そして、hGlobalにデータのハンドルを指定することになります。 CF_BITMAPの場合は、GetTextExtentPoint32でテキストのサイズを取得し、 その大きさのビットマップをCreateCompatibleBitmapで作成しています。 これに対してTextOutで文字列を描画すれば、 ビットマップをペーストした際にはそのテキストを確認できるようになります。 ビットマップはGDIオブジェクトであるため、tymedにはTYMED_GDIを指定し、 hBitmapにビットマップのハンドルを指定することになります。 なお、m_szTextはCDataObjectのコンストラクタで"sample"と初期化されています。

CF_TEXTのデータはIDataObject::GetDataHereで取得されることもあるため、 次のように処理しておく必要があります。

STDMETHODIMP CDataObject::GetDataHere(FORMATETC *pformatetc, STGMEDIUM *pmedium)
{
	if (pformatetc->cfFormat == CF_TEXT) {
		ULONG uWritten;
		pmedium->pstm->Write(m_szText, lstrlenA(m_szText) + 1, &uWritten);
	}
	else
		return E_FAIL;

	return S_OK;
}

GetDataHereでは、データを返す場所となるメモリが呼び出し側で既に作成されています。 たとえば、CF_TEXTの場合は、pmedium->pstmがデータを受け取るためのストリームを識別しています。 よって、このストリームのWriteを呼び出すことによって、ストリームにテキストを書き込むことができます。

最後に、今回取り上げた内容を通常のクリップボード関数で実現する例を示します。

OpenClipboard(hwnd);
EmptyClipboard();
SetClipboardData(CF_TEXT, hglobal);
SetClipboardData(CF_BITMAP, hbmpMem);
CloseClipboard();

必要なデータを用意しておけば、上記のようにSetClipboardDataを呼び出すことができます。 これにより、遅延レンダリングが発生しなくなりますから、 アプリケーションは終了しても問題ないことになります。

既定のIEnumFORMATETCについて

今回のプログラムではIEnumFORMATETCを明示的に実装しましたが、 システムにはIEnumFORMATETCを実装するオブジェクトが既定で存在しています。 よって、こうしたオブジェクトを使用するようになれば、IEnumFORMATETCを明示的に実装する必要がなくなります。

STDMETHODIMP CDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc)
{
	HRESULT   hr;
	FORMATETC formatetc[] = {
		{CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL | TYMED_ISTREAM},
		{CF_BITMAP, NULL, DVASPECT_CONTENT, -1, TYMED_GDI}
	};

	if (dwDirection == DATADIR_GET) {
		hr = CreateFormatEnumerator(2, formatetc, ppenumFormatEtc);
		return hr;
	}
	else
		return E_NOTIMPL;
}

CreateFormatEnumeratorにFORMATETC構造体とその数を指定すれば、既定のIEnumFORMATETCを取得することができますから、 後はこれを呼び出し側に返せばよいだけになります。 CreateFormatEnumeratorを呼び出す場合は、urlmon.libへのリンクが必要になります。

Windows Vistaからは、既定のIEnumFORMATETCを返すSHCreateStdEnumFmtEtcという関数も登場しました。 この関数の使い方はCreateFormatEnumeratorと同様です。

STDMETHODIMP CDataObject::EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC **ppenumFormatEtc)
{
	HRESULT   hr;
	FORMATETC formatetc[] = {
		{CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL | TYMED_ISTREAM},
		{CF_BITMAP, NULL, DVASPECT_CONTENT, -1, TYMED_GDI}
	};

	if (dwDirection == DATADIR_GET) {
		hr = SHCreateStdEnumFmtEtc(2, formatetc, ppenumFormatEtc);
		return hr;
	}
	else
		return E_NOTIMPL;
}

SHCreateStdEnumFmtEtcを呼び出す場合は、shlobj.hをインクルードする必要があります。



戻る