yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

OLE Drag & Dropを実装する

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Windows2000+Win32 SDKC言語を前提としており、2012年現在では内容が陳腐化している箇所もあります。

(当時の)ソースコードはこちら: https://gist.github.com/3053385


目的

Windowsの便利な機能に、ドラッグ・アンド・ドロップ(以下D&D)によるファイルのコピー・移動・ショートカット作成があります。これに相当する機能を実装してみようというのが今回の目的です。
ちなみに開発環境は、Win32 SDKベースのC言語を想定しています。

非OLE Drag&Drop

Explorer等からのファイルD&Dを実装する際に、最も簡単なのはDragAcceptFiles()関数を用いる方法です。
ファイルのドロップを受け付けたいウインドウのウインドウハンドル(hwnd)とすると、

HWND hwnd = /* 対象ウインドウのHWND */;

DragAcceptFiles(hwnd, TRUE);

とすることで、ファイルのドロップを受け付けるようになります。
対象ウインドウにExplorerからファイルがD&Dされると、ウインドウプロシージャーにWM_DROPFILESが送られるので、このメッセージをフックしてドロップされたファイルを処理してやります。

LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
  )
{
  HDROP hdrop;
  char filename[MAX_PATH];
  int num, i;

  switch (uMsg) {
  case WM_DROPFILES:
    hdrop = (HDROP)wParam;
    /* ドロップされたファイルの個数を得る */
    num = DragQueryFile(hdrop, -1, NULL, 0);
    /* ファイルを処理 */
    for (i = 0; i < num; i++) {
      DragQueryFile(hdrop, i, filename, &filename);
      /*
       * filename にファイル名が入っているので、ここで処理を行う。
       */
    }
    DragFinish(hdrop);
    break;

  /* other messages... */
  }
  return 0;
}

これでExplorerからのファイルドロップを実装できました。しかし、まだファイルD&Dのうち後者しか実現していません。
次はExplorerへのファイルドラッグの実装に挑戦してみます。

OLE

“ドラッグ”は“ドロップ”と対になっている動作ですが、Explorerへのファイル“ドラッグ”に関しては残念ながら上記のように単純には実装できません。これを行うにはOLE(Object Linking and Embedding)を利用する必要があります。OLEはCOM(Component Object Model)技術に基づいているため、実際はCOMサーバーを実装することが必要になってきます。
ここで問題になるのは、COMがオブジェクト指向であるためC言語で実装するのが面倒だということです。C++ならばclassを利用して比較的楽に実装できるのですが、Cにはそのような言語的サポートはありません。よって少々煩雑なコーディングが必要になってきます。
以下の解説ではCOMに関する説明は省略します。COMについて上手に説明しているサイトは沢山ありますのから、そちらを参考にして下さい。

OLE Drag&Drop

さて、OLE D&Dを実装するにあたって次の3つのCOMインターフェイスを使う必要があります。

  • IDataObject
  • IDropSource
  • IDropTarget

IDataObjectは受け渡しするデータ(ここではファイル)であり、IDropSourceはOLE D&Dのドラッグ開始元に、またIDropTargetはOLE D&Dのドロップ先に対応しています。そして、OLE D&Dのドラッグ開始するにはDoDragDrop()関数を用います。
OLE D&Dにおける処理の流れは次のようになっています。


  1. DoDragDrop()関数を呼び出す。
  2. ドラッグ中のオブジェクトがウインドウ領域の上に入ると、IDropTarget::DragEnter()が呼び出される。
  3. IDropSource::GiveFeedback()が呼び出される。
  4. マウスがウインドウ領域の上にいる間は次の関数が呼び出された後、IDropSource::GiveFeedback()が呼び出される。

    • マウスカーソルが領域内で動くと、IDropTarget::DragOver()が呼び出される。
    • マウスカーソルがウインドウ領域外に移動すると、IDropTarget::DragLeave()が呼び出される。

  5. ドラッグ中にキー入力やマウスクリックが発生するとIDropSource::QueryContinueDrag()が呼び出される。ここで、D&Dを完了させる(ドロップ)かキャンセルするかを決定する。
  6. オブジェクトがドロップされると、IDropTarget::Drop()が呼び出される。
  7. DoDragDrop()関数から戻る。このときIDropTarget側で設定されたDROPEFFECTが戻り値として得られる。

ここで、IDropSourceとIDropTargetは別々のアプリケーションに属している(ことが多い)に注意してください。
例えばExplorerからのファイルD&Dの場合、IDropSourceはExplorer側で提供され、IDropTargetは自作アプリケーション側で提供したものが使われます。

IDropSource

まず、ファイルの“ドラッグ”を実現するためのIDropSourceインターフェイスを実装します。IDropSourceには次の2つのメソッドがあります。

  • IDropSource::GiveFeedback()
  • IDropSource::QueryContinueDrag()

GiveFeedback()メソッドではドラッグ先(IDropTarget)で指定されたD&Dの状態(DROPEFFECT)によって、SetCursor()関数等でマウスカーソルにフィードバックを与えます。このときDRAGDROP_S_USEDEFAULTCURSORSを返すことで、OLEがデフォルトで提供するカーソル形状を利用できます。
QueryContinueDrag()メソッドではキー入力やマウス入力によって、D&Dを完了するかキャンセルするかを返します。Windowsの一般的なGUIとしては、ESCが押されたらキャンセルしマウスボタンが離されたらD&D完了(オブジェクトのドロップ)とします。

typedef struct tagOLEDropSource {
  IDropSource super;
  ULONG       m_cRefCnt;
} OLEDropSource;

/* IUnknown methods */
STDMETHODIMP OLEDropSource_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_Release(/*略*/);

/* IDropSource methods */
STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  BOOL fEscapePressed,
  DWORD grfKeyState
  )
{
  /* ESCキーが押されたらD&Dキャンセル */
  if (fEscapePressed)
    return DRAGDROP_S_CANCEL;
  /* マウスボタンが離されたらD&D完了 */
  if (!(grfKeyState & (MK_LBUTTON|MK_RBUTTON)))
    return DRAGDROP_S_DROP;
  return S_OK;
}

STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  DWORD dwEffect
  )
{
  /* OLEデフォルトのカーソルを使用する */
  return DRAGDROP_S_USEDEFAULTCURSORS;
}
IDataObject

次にOLE D&Dで受け渡されるデータを表現するためのIDataObjectインターフェイスを実装します。IDataObjectには次の9つのメソッドがあります。

  • IDataObject::GetData()
  • IDataObject::GetDataHere()
  • IDataObject::QueryGetData()
  • IDataObject::GetCanonicalFormatEtc()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()
  • IDataObject::DAdvise()
  • IDataObject::DUnadvise()
  • IDataObject::EnumDAdvise()

これら全てのメソッドを実装するのは大変な作業なのため、ここではファイルを表すHDROPを格納するための最低限の実装だけ行うことにします。
実装するのは次の4つのメソッドと、EnumFormatEtc()メソッドで必要になるIEnumFORMATETCインターフェイスです。

  • IDataObject::GetData()
  • IDataObject::QueryGetData()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()

ここではIDataObject/IEnumFORMATETCの実装の表記は省略します。

OLE D&D: Drag編

ではExplorerへのファイルドラッグのための前準備からD&D開始、後処理までの流れをコーディングします。

/*
 * ファイル名を'\0'で区切った文字列。末尾は2個の'\0'で終わる。
 */
char *files = "c:\file1.txt\0c:file2.txt\0\0";

IDataObject *lpDataObject = /* IDataObjectのインスタンス */;
IDropSource *lpDropSource = /* IDropSourceのインスタンス */;
FORMATETC fmtetc;
STGMEDIUM medium;
char *p;
DWORD effect;
HRESULT hr;


/* FORMATETC構造体をセット */
fmtetc.cfFormat = CF_HDROP;
fmtetc.ptd = NULL;
fmtetc.dwAspect = DVASPECT_CONTENT;
fmtetc.lindex = -1;
fmtetc.tymed = TYMED_HGLOBAL;

/* STGMEDIUM構造体をセット */
medium.tymed = TYMED_HGLOBAL;
medium.hGlobal = GlobalAlloc(
                   GMEM_MOVEABLE, 
                   sizeof(DROPFILES) + sizeof(files)
                 );
medium.pUnkForRelease = NULL;
p = GlobalLock(medium.hGlobal);
((DROPFILES *)p)->pFiles = sizeof(DROPFILES);
((DROPFILES *)p)->fWide = FALSE;
CopyMemory(p + sizeof(DROPFILES), files, sizeof(files));
GlobalUnlock(medium.hGlobal);

/* IDataObjectをセット */
IDataObject_SetData(lpDataObejct, &fmtetc, &medium, FALSE);

/* OLE Drag & Drop開始 */
hr = DoDragDrop(
       lpDataObejct,
       lpDropSource,
       DROPEFFECT_MOVE|DROPEFFECT_COPY|DROPEFFECT_LINK,
       &effect
     );

/* リソースを開放 */
IDataObject_Release(lpDataObejct);
IDropSource_Release(lpDropSource);

/*
 * hr, effectの値によってデータを処理...
 */

実際のコードはマウスのドラッグ開始メッセージなどによって実行されるようにします。これはWM_LBUTTONDOWN, WM_RBUTTONDOWN, LVN_BEGINDRAG(リストビュー), TVN_BEGINDRAG(ツリービュー)等が相当します。
これでExplorerへのファイルD&Dが実装できました。最初の非OLE D&DのExplorerからのファイルドロップ受付と、この実装を組み合わせればExplorerとの完全なD&Dが実現できます。
しかし、せっかくOLE D&Dの“ドラッグ”機能を実装したので、“ドロップ”機能もOLEを利用して実装することにします。

IDropTarget

では、ファイルの“ドロップ”を実現するためのIDropTargetインターフェイスを実装します。IDropTargetには次の4つのメソッドがあります。

  • IDropTarget::DragEnter()
  • IDropTarget::DragOver()
  • IDropTarget::DragLeave()
  • IDropTarget::Drop()

DragEnter()/DragOver()/DragLeave()メソッドは対象ウインドウ領域内にマウスカーソルが"入った"/"移動した"/"出た"という動きに対応して呼び出されます。これらのメソッド内でキー入力に応じてDROPEFFECTを指定することにより、IDropSourceが視覚的フィードバックを行なうめの情報を渡します。
Drop()メソッドは実際にオブジェクトが対象ウインドウにドロップされると呼び出されます。ここでIDataObjectを介して実際にデータのやり取りを行ないます。
今回はOLE D&Dのターゲットウインドウ側の(視覚的な)フィードバックは考慮していませんが、何らかのフィードバックを行いたい場合は各メソッドでリソースの初期化/破棄を行なう必要があります。

typedef struct tagOLEDropTarget {
  IDropTarget super;
  ULONG       m_cRefCnt;
} OLEDropTarget;


/* IUnknown methods */
STDMETHODIMP OLEDropTarget_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_Release(/*略*/);

/* IDropTarget methods */
STDMETHODIMP OLEDropTarget_DragEnter(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * pDataObject に応じてドロップを受け付けるかを判断
   * ドロップを拒否する際は
   *   pdwEffect = DROPEFFECT_NONE;
   * とする。
   */
  
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragOver(
  OLEDropTarget *this,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragLeave(
  OLEDropTarget *this
  )
{
  return S_OK;
}

STDMETHODIMP OLEDropTarget_Drop(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,D
  WORD *pdwEffect
  )
{
  /*
   * D&D完了時の処理...
   */
  return S_OK;
}

OLE D&D: Drop編

IDropTargetの準備ができたら、Explorerからのファイルドロップを受け付けるようコーディングしてみます。

RegisterDragDrop()関数を用いて、IDropTargetインターフェイスとファイルD&Dを受け付けるウインドウを関連付けます。

HWND hwnd = /* 対象ウインドウのハンドル */;
IDropTarget *lpDropTarget = /* IDropTargetのインスタンス */;

RegisterDragDrop(hwnd, lpDropTarget);

これでOLE D&Dの受け入れが登録され、ファイルがドロップされると関連付けたIDropTargetのDrop()メソッドが呼び出されるようになりました。
逆に登録を解除するにはRevokeDragDrop()関数を用いて次のようにします。

HWND hwnd = /* 対象ウインドウのハンドル */;

RevokeDragDrop(hwnd);

なおIDropTargetの参照は登録時にAddRef()/解除時にRelease()されるので明示的にRelease()する必要はありません。

OLE D&D: まとめ

これらのOLE D&Dを利用するためには、OLEライブラリの初期化を先立って行なわなければいけません。これはOleInitialize()関数で行ないます。また、利用しなくなったらOleUninitialize()関数で解放する必要があります。
これらはWinMain()で他の初期化/解放処理と一緒に行なったほうが良いでしょう。

int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow
  )
{
  /* OLEライブラリを初期化 */
  OleInitialize(NULL);
  
  /*
   * メイン処理...
   */
  
  /* ライブラリを解放 */
  OleUninitialize();
  return 0;
}

あとがき

これで当初の目的であるExplorerとのファイルD&Dは実装できました。ただしIDataObjectの実装は手抜きですからIAdviseSinkを利用できません。これはソース側にオブジェクト(IDataObject)の変更を通知するためのインターフェイスです。また、Windows2000からはIDropSourceHelper/IDropTargetHelperというインターフェイスが追加されて、視覚的フィードバックの実装の手助けをしてくれるようです。
今回利用したOLE D&DではファイルのD&D以外にも様々なデータ(オブジェクト)のD&Dに対応しています。例えばMicrosoft Word等では選択したテキストをD&Dによってコピー&ペーストすることができますが、このようなテキストもOLE D&Dで受け取ることができます。
これらについての情報は、IDataObjectやFORMATETCといったキーワードから検索できると思います。