Shell Drag/Drop Helper オブジェクト
第 2 部 : IDropSourceHelper
Raymond Chen
Microsoft Corporation
February 2000
日本語版最終更新日 2000年5月10日
要約 : Microsoft® Windows® 2000 シェルには、新しいオブジェクトである shell drag/drop helper オブジェクトが用意されています。このオブジェクトを使用すると、ドラッグ イメージやアルファ ブレンディングなどをはじめとする shell drag/drop ユーザー インターフェイスを利用できます。この記事は、アプリケーションで shell drag/drop helper オブジェクトを使ってユーザーの操作性を高める方法を解説した 2 部構成の記事の第 2 部です。
Contents
IDragSourceHelper インターフェイスにアクセスする
IDataObject を準備する
CDataObject サンプル
IDragSource を準備する
ゴール
図 1. カスタム ドラッグ イメージの使用
図 1 は、ドラッグ/ドロップ フィードバック イメージとしてピクチャをそのまま (イメージを作成する時間がないなどの理由のある場合に) 使用するサンプル プログラムです。"実際の" アプリケーションでは、ドラッグされるオブジェクトのビットマップ イメージなどが使用されます。
IDragSourceHelper インターフェイスにアクセスする
IDragSourceHelper インターフェイスを使用することにより、ドラッグ/ドロップ フィードバックの一部としてシェル (または、shell drag/drop インターフェイスを使用するそのほかのオブジェクト) が使用するイメージを提供することができます。ただし、このインターフェイスは、IDropTargetHelper ほど簡単には使用できません。
ドラッグ/ドロップ操作では、 IDragSource と IDataObject という 2 つのオブジェクトが使用されます。両方のオブジェクトとも、IDragSourceHelper インターフェイスを利用するためには、それぞれ特別な準備を必要とします。
IDataObject を準備する
このデータ オブジェクトの重要な属性は、 IDataObject::SetData メソッドの中で任意のクリップボード形式とメディア型を受け取ることができなければならないことです。つまり、保持している配列またはそのほかのデータ構造体に、 IDataObject::SetData メソッドが呼び出されたときに受け取る FORMATETC オブジェクトと STGMEDIUM オブジェクトを記録し、要求がありしだいこれらの形式を提供して、これらの形式を使用できるように列挙することが必要です。
この記事では、サンプル コードを紹介することはできませんが、重要な部分のみをピックアップして説明します。
CDataObject サンプル
ここでは、上記の要件を満たすデータ オブジェクトの実装方法を簡単に説明します。ここでは、任意のクリップボード形式とメディア型を受け取るデータ オブジェクトを作成する 1 つの方法を例として紹介するだけなので、このテクニックを必ず使用しなければならないということではありません。
データ オブジェクトの基本概念は、関連する STGMEDIUM 構造体と共に、設定されたすべての FORMATETC 構造体の配列をデータ オブジェクトの中に置いておくという簡単なものです。ただし、誰もがご存じのとおり、問題となりやすいのは細かな部分です。
まず、クラス宣言から始めます。
class CDataObject : public IDataObject, public IPersist {
...
private:
typedef struct {
FORMATETC fe;
STGMEDIUM stgm;
} DATAENTRY, *LPDATAENTRY; /* アクティブな FORMATETC ごとにこれらのうちの 1 つを取得 */
HRESULT FindFORMATETC(FORMATETC *pfe, LPDATAENTRY *ppde, BOOL fAdd);
HRESULT AddRefStgMedium(STGMEDIUM *pstgmIn, STGMEDIUM *pstgmOut, BOOL fCopyIn);
LPDATAENTRY m_rgde; /* アクティブな DATAENTRY エントリの配列 */
int m_cde; /* m_rgde のサイズ */
CDataObject() : m_rgde(NULL), m_cde(0) { }
~CDataObject();
};
デストラクタにより、データ オブジェクトが割り当てた、または所有者であったメモリをすべて解放します。
CDataObject::~CDataObject()
{
for (int ide = 0; ide < m_cde; ide++) {
CoTaskMemFree(m_rgde[ide].fe.ptd);
ReleaseStgMedium(&m_rgde[ide].stgm);
}
CoTaskMemFree(m_rgde);
}
CDataObject::FindFORMATETC メソッドが、データ オブジェクト内の提供された FORMATETC 構造体を検索 (および追加) します。値が検索されて追加されない場合、要求された TYMED がサポートされているかどうかも確認されます。
HRESULT
CDataObject::FindFORMATETC(FORMATETC *pfe, LPDATAENTRY *ppde, BOOL fAdd)
{
*ppde = NULL;
/* 2 つの DVTARGETDEVICE 構造体の比較は難しいので行わない */
if (pfe->ptd != NULL) return DV_E_DVTARGETDEVICE;
/* リストに含まれているかどうかを確認 */
for (int ide = 0; ide < m_cde; ide++) {
if (m_rgde[ide].fe.cfFormat == pfe->cfFormat &&
m_rgde[ide].fe.dwAspect == pfe->dwAspect &&
m_rgde[ide].fe.lindex == pfe->lindex) {
if (fAdd || (m_rgde[ide].fe.tymed & pfe->tymed)) {
*ppde = &m_rgde[ide];
return S_OK;
} else {
return DV_E_TYMED;
}
}
}
if (!fAdd) return DV_E_FORMATETC;
LPDATAENTRY pdeT = (LPDATAENTRY)CoTaskMemRealloc(m_rgde,
sizeof(DATAENTRY) * (m_cde+1));
if (pdeT) {
m_rgde = pdeT;
m_cde++;
m_rgde[ide].fe = *pfe;
ZeroMemory(&pdeT[ide].stgm, sizeof(STGMEDIUM));
*ppde = &m_rgde[ide];
return S_OK;
} else {
return E_OUTOFMEMORY;
}
}
CDataObject::AddRefStgMedium メソッドで、 STGMEDIUM 構造体がコピーされ、参照カウントがインクリメントされます。この際に、メディア型が TYMED_HGLOBAL で pUnkForRelease がない場合、あいまいな状況が起こります。データを外部ソースから受け取ると、データ オブジェクトは HGLOBAL の内容をコピーしなければなりませんが、データが外部ソースに戻される場合は、 pUnkForRelease として、そのデータ オブジェクトそのもの (参照カウントされる) を設定することにより、コピーが行われないようにすることができます。
HRESULT
CDataObject::AddRefStgMedium(STGMEDIUM *pstgmIn, STGMEDIUM *pstgmOut, BOOL fCopyIn)
{
HRESULT hres = S_OK;
STGMEDIUM stgmOut = *pstgmIn;
if (pstgmIn->pUnkForRelease == NULL &&
!(pstgmIn->tymed & (TYMED_ISTREAM | TYMED_ISTORAGE))) {
if (fCopyIn) {
/* オブジェクトのコピーが必要 */
if (pstgmIn->tymed == TYMED_HGLOBAL) {
stgmOut.hGlobal = GlobalClone(pstgmIn->hGlobal);
if (!stgmOut.hGlobal) {
hres = E_OUTOFMEMORY;
}
} else {
hres = DV_E_TYMED; /* GDI オブジェクトのコピー方法がわからない */
}
} else {
stgmOut.pUnkForRelease = static_cast<IDataObject*>(this);
}
}
if (SUCCEEDED(hres)) {
switch (stgmOut.tymed) {
case TYMED_ISTREAM:
stgmOut.pstm->AddRef();
break;
case TYMED_ISTORAGE:
stgmOut.pstg->AddRef();
break;
}
if (stgmOut.pUnkForRelease) {
stgmOut.pUnkForRelease->AddRef();
}
*pstgmOut = stgmOut;
}
return hres;
}
CDataObject::AddRefStgMedium メソッドは、次のヘルパー関数を使用します。
HGLOBAL GlobalClone(HGLOBAL hglobIn)
{
HGLOBAL hglobOut = NULL;
LPVOID pvIn = GlobalLock(hglobIn);
if (pvIn) {
SIZE_T cb = GlobalSize(hglobIn);
HGLOBAL hglobOut = GlobalAlloc(GMEM_FIXED, cb);
if (hglobOut) {
CopyMemory(hglobOut, pvIn, cb);
}
GlobalUnlock(hglobIn);
}
return hglobOut;
}
これらのヘルパー関数を使用すると、IDataObject インターフェイスのデータ転送メソッドの実装が簡単になり、あいまいさを避けることができます。
CDataObject::GetData メソッドが適切な DATAENTRY 構造体を見つけ、呼び出し側にデータを戻すために CDataObject::AddRefStgMedium ヘルパー メソッドを使用します。
HRESULT CDataObject::GetData(FORMATETC *pfe, STGMEDIUM *pstgm)
{
LPDATAENTRY pde;
HRESULT hres = FindFORMATETC(pfe, &pde, FALSE);
if (SUCCEEDED(hres)) {
hres = AddRefStgMedium(&pde->stgm, pstgm, FALSE);
}
return hres;
}
CDataObject::QueryGetData メソッドは、実際にデータを戻すことなく、 GetData の動作を確認します。
HRESULT CDataObject::QueryGetData(FORMATETC *pfe)
{
LPDATAENTRY pde;
return FindFORMATETC(pfe, &pde, FALSE);
}
CDataObject::SetData CDataObject::SetData メソッドは、一致する DATAENTRY 構造体を見つけます (適宜、この構造体を作成したり、古いデータを解放します)。呼び出し側がそのオブジェクトの所有権を転送する場合 (fRelease = TRUE)、参照カウントを調整することなく、ストレージ メディアをコピーします。転送しない場合には、 CDataObject::AddRefStgMedium ヘルパー メソッドを使って、ストレージ メディアをコピーし、関連する DATAENTRY 構造体に参照を保持します。
HRESULT CDataObject::SetData(FORMATETC *pfe, STGMEDIUM *pstgm, BOOL fRelease)
{
if (!fRelease) return E_NOTIMPL;
LPDATAENTRY pde;
HRESULT hres = FindFORMATETC(pfe, &pde, TRUE);
if (SUCCEEDED(hres)) {
if (pde->stgm.tymed) {
ReleaseStgMedium(&pde->stgm);
ZeroMemory(&pde->stgm, sizeof(STGMEDIUM));
}
if (fRelease) {
pde->stgm = *pstgm;
hres = S_OK;
} else {
hres = AddRefStgMedium(pstgm, &pde->stgm, TRUE);
}
pde->fe.tymed = pde->stgm.tymed; /* 同期で維持 */
/* あいまい発生! 循環参照ループを抜ける */
if (GetCanonicalIUnknown(pde->stgm.pUnkForRelease) ==
GetCanonicalIUnknown(static_cast<IDataObject*>(this))) {
pde->stgm.pUnkForRelease->Release();
pde->stgm.pUnkForRelease = NULL;
}
}
return hres;
}
このコードは非常にあいまいです。
クライアントにより、pUnkForRelease メンバがそのデータ オブジェクトそのものへのポインタになっている STGMEDIUM 構造体が渡される場合、循環参照ループが発生します。このような場合、クライアントが IDataObject::GetData メソッドを呼び出し、それからまた、同じ STGMEDIUM 構造体を IDataObject::SetData メソッドに渡すということが起こります。循環参照が続く場合、データ オブジェクトが残ったままになります。
循環参照を検出するには、 pUnkForRelease がそのデータ オブジェクトそのものを参照しているかどうかを確認することが必要です。このポインタは OLE のマーシャラを介して渡されるので、変更することのないポインタの数値を使用することはできません。たいてい、このポインタは、マーシャリング スタブ オブジェクトをポイントしています。したがって、COM オブジェクト ID の規則を使用します。 IUnknown::QueryInterface メソッドを明示的に呼び出すと、 IUnknown インターフェイスが要求され、必ず同じポインタが戻されます。
このためには、次のヘルパー関数を使用し、正しい IUnknown へ参照カウントされないポインタを戻します。
IUnknown *GetCanonicalIUnknown(IUnknown *punk)
{
IUnknown *punkCanonical;
if (punk && SUCCEEDED(punk->QueryInterface(IID_IUnknown,
(LPVOID*)&punkCanonical))) {
punkCanonical->Release();
} else {
punkCanonical = punk;
}
return punkCanonical;
}
しかし、ここには 2 重のあいまいさがあります。static_cast(this)
の結果をテストするだけで、(CDataObject::QueryInterface メソッドがその正しい IUnknown としてその同じ値を使用することを前提として) そのデータ オブジェクトそのものへの参照を検出することができるようにも思われますが、データ オブジェクトが集合体の場合、これは機能しません。なぜなら、集合オブジェクトの正しい IUnknown は、最も外側の不明な制御に対する正しい IUnknown であるからです。
したがって、自己参照を適切に検出するためには、面倒な GetCanonicalUnknown メソッドを介してデータ オブジェクトを渡すことが必要です。
最後に、3 重のあいまいさが存在します。自己参照がそのデータ オブジェクトそのものであることを検出すると、 pUnkForRelease 上で IUnknown::Release メソッドを呼び出して安全であることがなぜわかるのでしょうか。それは、呼び出し側が確保している IDataObject ポインタ内にそのデータ オブジェクトへの参照がそのまま置かれていることから、自身を解放してもデータ オブジェクトを破棄することにはならないためです。
データ オブジェクトを適切に実装する方法に説明がそれてしまいましたが、Shell Drag/Drop Helper オブジェクトに必要なオブジェクトを準備するという本題に戻りましょう。
IDragSource を準備する
まず、 CoCreateInstance 関数で Shell Drag/Drop Helper オブジェクトを作成し、IID_IDragSourceHelper をインターフェイスとして指定します。IDropTargetHelper インターフェイスと同様、複数のドラッグ ソースがお互いに邪魔することのないように、各ドラッグ ソースに、このオブジェクトのコピーを渡すことが必要です。
IDragSourceHelper *m_pdsh;
/* この呼び出しが失敗すると、OLE が m_pdsh = NULL を設定 */
CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER,
IID_IDropTargetHelper, (LPVOID*)&m_pdsh);
また、Shell Drag/Drop Helper オブジェクトは Windows 2000 の新機能なので、前のバージョンの Windows では、このヘルパー オブジェクトは作成できません。したがって、このインターフェイスを使用する前に、きちんと NULL のチェックを行うことが必要です。
DoDragDrop 関数を呼び出す直前に、ドラッグ ソースは、データ オブジェクトに、ドラッグするアイテムのイメージを格納することが必要です。これには、次の 2 つの方法があります。
ビットマップから初期設定する
IDragSourceHelper::InitializeFromBitmap メソッドを使用することにより、ドラッグされるオブジェクトを表すビットマップを (関連情報と共に) 提供することができます。ビットマップと座標に加えて、 SHDRAGIMAGE 構造体に色キーも提供します。色が色キーと一致するソース ビットマップの全ピクセルは、透明な色として処理されます。図 1 の画面イメージを作成する際に
GetSysColor(COLOR_WINDOW)
を色キーとして使用しました。これにより、ウィンドウの内部が透明になっています。SHDRAGIMAGE 構造体を埋めたら、これとデータ オブジェクトを IDragSourceHelper::InitializeFromBitmap メソッドに渡します。この関数が成功すると、Shell Drag/Drop Helper オブジェクトはそのビットマップの所有権を持ちます。失敗した場合、メモリを無駄にしないため、ビットマップを破棄することが必要です。
ウィンドウから初期設定する
イメージの生成をウィンドウから行う方法もあります。ウィンドウ プロシージャが、DI_GETDRAGIMAGE 登録ウィンドウ メッセージを処理し、応答して、 SHDRAGIMAGE 構造体に適切な情報を埋め込みます。 SHDRAGIMAGE から戻されるビットマップは、必要がなくなれば Shell Drag/Drop Helper オブジェクトによって破棄されるので、明示的に破棄する必要はありません。
関連ウィンドウはドラッグ ソースと同じプロセス内に置く必要があります。これは、DI_GETDRAGIMAGE メッセージが SHDRAGIMAGE 構造体へのポインタを lParam として渡すためです。
ゴール
データ オブジェクト (もちろん、ドラッグされている実際のオブジェクトが含まれていることも必要です) の IDataObject::SetData メソッドを使用できるようにし、上述した 2 つのいずれかの方法でデータ オブジェクトにイメージを設定したら、 DoDragDrop を呼び出すことができます。すると、ドラッグされるオブジェクトのイメージがアルファ ブレンドされて表示されます。