Shell IDList Arrayについて

クリップボードフォーマットにおいて、Shell IDList Arrayを用いた通信について記事が少ないので一言。

CIDAのフォーマットの定義は以下の通りだ。

typedef struct _IDA {
    UINT cidl; // IDLISTの数
    UINT aoffset[1]; // IDLIST構造体のリスト
} CIDA, * LPIDA;

実際には可変長構造体でaoffsetは0~cidlの序数を持つ。定義ではそうは読み取れないが、Win32apiやDLL周りを扱う場合には頻繁に出てくる表現だ。
ここで、aoffsetの個数がcidl+1個なのは間違いではない。

aoffset[0]は、フォルダーのIDLISTへのポインター、aoffset[1]~aoffset[cidl]は、フォルダー内でドラッグアンドドロップされているファイルのIDLISTへのポインターだ。
前者は絶対参照、後者は相対参照なので、ファイルのIDLISTを使ってSHGetPathFromIDListを呼び出しても正しいパスは得られない。

ファイルを得るためのコードを以下に示す。

// Shell IDList Arrayのクリップボードフォーマットがあるか確認
CLIPFORMAT get_ShellIDListArray_ID( IDataObject *pDataObj )
{
  TCHAR          szFormatName[256];
  unsigned int   nSize = sizeof(szFormatName) / sizeof(TCHAR);
  FORMATETC      formatetc;
  IEnumFORMATETC *pEnumFromatetc;
  pDataObj->EnumFormatEtc( DATADIR_GET, &pEnumFromatetc );

  while( S_OK == pEnumFromatetc->Next( 1, &formatetc, NULL ) )
  {
    ::GetClipboardFormatName( formatetc.cfFormat, 
                                       szFormatName, nSize );
    if( 0 == ::lstrcmp( szFormatName, CFSTR_SHELLIDLIST ) )
      break;
    formatetc.cfFormat = 0;
  }

  pEnumFromatetc->Release();
  return formatetc.cfFormat;
}

// ポインター操作用補助関数
BYTE* byte_cast( LPIDA pIda, UINT p )
{
  return reinterpret_cast<BYTE*>(pIda)+pIda->aoffset[p];
}

// IDropTargetの実装として定義したTDropTargetのメンバー関数 
STDMETHODIMP TDropTarget::Drop(IDataObject *pDataObj,
           DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
  FORMATETC formatetc;

  formatetc.cfFormat = get_ShellIDListArray_ID( pDataObj );
  formatetc.ptd      = NULL;
  formatetc.dwAspect = DVASPECT_CONTENT;
  formatetc.lindex   = -1;
  formatetc.tymed    = TYMED_HGLOBAL;

  if( 0 == fotmatetc.cfFormat )
    return E_FAIL;
    
  // クリップボードデータの取り込み
  STGMEDIUM medium;
  if( S_OK != pDataObj->GetData( &formatetc, &medium ) )
    return E_FAIL;

  if( medium.tymed != TYMED_HGLOBAL )
  {
    ::ReleaseStgMedium( &medium );
    return E_FAIL;
  }

  // 内部データのメモリー展開
  CIDA *pIda = static_cast<CIDA*>(::GlobalLock( medium.hGlobal ));

  // フォルダーIDLISTへのポインター
  PIDLIST_ABSOLUTE pidl_root = 
     reinterpret_cast<PIDLIST_ABSOLUTE>( byte_cast( pIda, 0 ) );

  // ファイルのリストアップ
  for( unsigned int i = 1; i <= pIda->cidl; ++i ) // 各ファイルの序数は1~cidl
  {
    // ファイルへのIDLISTへのポインター
    PIDLIST_RELATIVE pidl_file = 
       reinterpret_cast<PIDLIST_RELATIVE>( byte_cast( pIda, i ) );

    // ファイルへのフルパスのIDLISTを構成
    PIDLIST_ABSOLUTE pidl = ::ILCombine( pidl_root, pidl_file );

    // ファイル名を得る
    TCHAR szSrc[MAX_PATH];
    ::SHGetPathFromIDList( pidl, szSrc );
    // ・・・なにがしかの処理

    // ファイルフルパスのIDLISTのメモリーを解放
    ::ILFree( pidl );
  }
  
  // メモリー展開を解除
  ::GlobalUnlock(medium.hGlobal);

  // クリップボード構造体の解放
  ::ReleaseStgMedium(&medium);
}

ILCombineを用いて、絶対パスのPIDLIST_ABSOLUTEを得る。使用を終えたら解放を忘れずに。
あと、SHGetPathFromIDListではUWPプログラムへのパスを得られない。Microsoft Edgeのパスは得られないなど、UWPのプログラムへのパスを得るとか大変面倒だ。