EternalWindows
OLE D&D / シェルとD&D

これまで、OLE D&DをサポートするアプリケーションとしてWordを取り上げてきましたが、 OLE D&Dはこれ以外にも様々なアプリケーションで使用されています。 たとえば、Visual Studioではエディタに入力されたテキストを別の位置にD&Dすることができますし、 シェルではファイルやフォルダをD&Dすることができます。 ただし、このシェルにおけるD&Dは少しだけ他とは異なり、 ドラッグされるデータのイメージが表示されるという特徴があります。

上図は、シェルから今回作成したアプリケーションにファイルをドロップしようとしているところですが、 データのイメージが表示されていることが分かります。 実は、シェルでドラッグするIDataObjectにはビットマップのイメージが格納されており、 データを受け取る側であるドロップターゲットがIDropTargetHelperを使用するようになれば、 このイメージをウインドウ上に表示できるようになります。 D&Dを対象としてシェルを想定するアプリケーションは、このような点についても理解しておく必要があります。

IDataObjectにイメージを設定するのは、IDragSourceHelperです。 これは、ドロップソース側が使用するインターフェースになります。 IDropTargetHelperがデータを表示できるのは、ドロップソースがIDragSourceHelperを使用している場合に限られるため、 たとえばWordから図形を受け取るドロップターゲットがIDropTargetHelperを使用しても、 イメージが表示されることはない点に注意してください。 また、ドロップソースがIDragSourceHelperを使用していても、 ドロップターゲットがIDropTargetHelperを使用していない場合は、 イメージが表示されることはありません。 なお、IDragSourceHelperとIDropTargetHelperはWindows 2000から使用可能です。

基本的に、IDragSourceHelperやIDropTargetHelperを使用するのは、 シェル上のファイルを受け取ったり、シェル上にファイルをドロップしたりする場合に限られるでしょう。 このようなドロップの際には、IDataObjectを実装したオブジェクトがファイルに関するデータを維持する必要がありますが、 幸いにもそうしたオブジェクトを明示的に作成する必要はありません。 理由は、シェル名前空間のインターフェースであるIShellFolder::GetUIObjectOfを使用すれば、 特定のアイテムに関連するIDataObjectを取得できるからです。

シェルからファイルを受け取るためには、IDropTargetを実装したオブジェクトが必要になりますが、 これについてはIShellFolder::CreateViewObjectを呼び出すことで取得できます。 よって、これをRegisterDragDropに指定すればCDropTargetのようなクラスは不要になりますが、 CreateViewObjectで取得できるIDropTargetはIDropTargetHelperを使用していないため、 イメージが表示されないという問題が発生することになります。 このため、当初はIDropTargetをCDropTargetのメンバとして維持し、 これとIDropTargetHelperを適切なタイミングで使用すればよいと考えたのですが、 何故かこのような用途にはIDropTargetが上手く機能しませんでした。 この結果、今回はファイルがドロップされた際の処理を明示的に行っています。

今回のプログラムは、カレントディレクトリのファイルとフォルダをリストビューに列挙し、 これをドラッグできるようにします。 また、シェルからファイルをドロップすることもでき、 そのような場合はカレントディレクトリもリストビューも更新されます。

#include <windows.h>
#include <commctrl.h>
#include <shlobj.h>
#include <shlwapi.h>

#define WM_SHELLCHANGE WM_APP

#pragma comment (lib, "shlwapi.lib")
#pragma comment (lib, "comctl32.lib")

class CDropTarget : public IDropTarget
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect);
	STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect);
	STDMETHODIMP DragLeave();
	STDMETHODIMP Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect);

	CDropTarget(HWND hwnd);
	~CDropTarget();
	DWORD GetEffectFromKeyState(DWORD grfKeyState);
	void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId);

private:
	LONG              m_cRef;
	HWND              m_hwnd;
	BOOL              m_bSupportFormat;
	BOOL              m_bRButton;
	IDropTargetHelper *m_pDropTargetHelper;
};

class CDropSource : public IDropSource
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState);
	STDMETHODIMP GiveFeedback(DWORD dwEffect);

	CDropSource();
	~CDropSource();

private:
	LONG m_cRef;
};

TCHAR g_szBaseDirectory[256];

BOOL SetDragImage(HWND hwnd, PIDLIST_ABSOLUTE *ppidlAbsolute, IDragSourceHelper *pDragSourceHelper, IDataObject *pDataObject);
void InsertListViewItem(HWND hwndListView);
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 HWND              hwndListView = NULL;
	static IDropTarget       *pDropTarget = NULL;
	static IDropSource       *pDropSource = NULL;
	static IDragSourceHelper *pDragSourceHelper = NULL;
	
	switch (uMsg) {
		
	case WM_CREATE: {
		HIMAGELIST           himl;
		SHFILEINFO           fileInfo;
		SHChangeNotifyEntry  entry;
		PIDLIST_ABSOLUTE     pidlDesktop; 
		INITCOMMONCONTROLSEX ic;

		OleInitialize(NULL);
		pDropTarget = new CDropTarget(hwnd);
		pDropSource = new CDropSource();
		CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDragSourceHelper));
		RegisterDragDrop(hwnd, pDropTarget);
		
		ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
		ic.dwICC  = ICC_LISTVIEW_CLASSES;
		InitCommonControlsEx(&ic);
		
		hwndListView = CreateWindowEx(0, WC_LISTVIEW, TEXT(""), WS_CHILD | WS_VISIBLE | LVS_ICON, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		himl = (HIMAGELIST)SHGetFileInfo((LPCTSTR)TEXT("C:\\"), 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX | SHGFI_LARGEICON);
		ListView_SetImageList(hwndListView, himl, LVSIL_NORMAL);
		InsertListViewItem(hwndListView);
		
		SHGetSpecialFolderLocation(hwnd, CSIDL_DESKTOP, &pidlDesktop);
		entry.pidl = pidlDesktop;
		entry.fRecursive = TRUE;
		SHChangeNotifyRegister(hwnd, SHCNRF_ShellLevel | SHCNRF_NewDelivery, SHCNE_CREATE | SHCNE_DELETE | SHCNE_MKDIR | SHCNE_RMDIR, WM_SHELLCHANGE, 1, &entry);

		GetCurrentDirectory(sizeof(g_szBaseDirectory) / sizeof(TCHAR), g_szBaseDirectory);

		return 0;
	}
	
	case WM_SHELLCHANGE: {
		LONG             lEvent;
		HANDLE           hLock;
		PIDLIST_ABSOLUTE *pidlAbsolute;
		TCHAR            szFilePath[256];
	
		hLock = SHChangeNotification_Lock((HANDLE)wParam, (DWORD)lParam, &pidlAbsolute, &lEvent);
		if (hLock != NULL) {
			SHGetPathFromIDList(pidlAbsolute[0], szFilePath);
			PathRemoveFileSpec(szFilePath);
			if (lstrcmpi(g_szBaseDirectory, szFilePath) == 0) {
				ListView_DeleteAllItems(hwndListView);
				InsertListViewItem(hwndListView);
			}
			SHChangeNotification_Unlock(hLock);
		}
		return 0;
	}
	
	case WM_NOTIFY:
		if (((LPNMHDR)lParam)->code == LVN_BEGINDRAG) {
			int              i, j;
			int              nCount, nSelectedCount;
			TCHAR            szText[256];
			TCHAR            szFilePath[256];
			PITEMID_CHILD    *ppidlChild;
			PIDLIST_ABSOLUTE *ppidlAbsolute;
			IDataObject      *pDataObject;
			IShellFolder     *pShellFolder = NULL;
			DWORD            dwEffect;
			
			nSelectedCount = ListView_GetSelectedCount(hwndListView);
			if (nSelectedCount == 0)
				return 0;

			ppidlAbsolute = (PIDLIST_ABSOLUTE *)CoTaskMemAlloc(sizeof(PIDLIST_ABSOLUTE) * nSelectedCount);
			ppidlChild = (PITEMID_CHILD *)CoTaskMemAlloc(sizeof(PITEMID_CHILD) * nSelectedCount);

			nCount = ListView_GetItemCount(hwndListView);
			for (i = 0, j = 0; i < nCount; i++) {
				if (ListView_GetItemState(hwndListView, i, LVIS_SELECTED)) {
					ListView_GetItemText(hwndListView, i, 0, szText, sizeof(szText) / sizeof(TCHAR));
					wsprintf(szFilePath, TEXT("%s\\%s"), g_szBaseDirectory, szText);
					ppidlAbsolute[j++] = ILCreateFromPath(szFilePath);
				}
			}
			
			SHBindToParent(ppidlAbsolute[0], IID_PPV_ARGS(&pShellFolder), NULL);

			for (i = 0; i < nSelectedCount; i++)
				ppidlChild[i] = ILFindLastID(ppidlAbsolute[i]);
			pShellFolder->GetUIObjectOf(NULL, nSelectedCount, (LPCITEMIDLIST *)ppidlChild, IID_IDataObject, NULL, (void **)&pDataObject);
			
			SetDragImage(hwnd, ppidlAbsolute, pDragSourceHelper, pDataObject);
			DoDragDrop(pDataObject, pDropSource, DROPEFFECT_MOVE | DROPEFFECT_COPY | DROPEFFECT_LINK, &dwEffect);

			for (i = 0; i < nSelectedCount; i++)
				CoTaskMemFree(ppidlAbsolute[i]);
			CoTaskMemFree(ppidlAbsolute);
			CoTaskMemFree(ppidlChild);
		}
		else
			;
		return 0;
	
	case WM_SIZE:
		MoveWindow(hwndListView, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		if (pDropTarget != NULL)
			pDropTarget->Release();
		if (pDropSource != NULL)
			pDropSource->Release();
		if (pDragSourceHelper != NULL)
			pDragSourceHelper->Release();
		RevokeDragDrop(hwnd);
		OleUninitialize();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

BOOL SetDragImage(HWND hwnd, PIDLIST_ABSOLUTE *ppidlAbsolute, IDragSourceHelper *pDragSourceHelper, IDataObject *pDataObject)
{
	HRESULT     hr;
	HDC         hdc, hdcMem;
	HBITMAP     hbmp, hbmpPrev;
	SHFILEINFO  fileInfo;
	SHDRAGIMAGE dragImage;
	POINT       pt = {0, 0};
	SIZE        size = {32, 32};

	SHGetFileInfo((LPCTSTR)ppidlAbsolute[0], 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_ICON | SHGFI_LARGEICON);

	hdc = GetDC(hwnd);
	hdcMem = CreateCompatibleDC(hdc);
	hbmp = CreateCompatibleBitmap(hdc, size.cx, size.cy);
	hbmpPrev = (HBITMAP)SelectObject(hdcMem, hbmp);
	DrawIcon(hdcMem, 0, 0, fileInfo.hIcon);
	SelectObject(hdcMem, hbmpPrev);
	DeleteDC(hdcMem);
	ReleaseDC(hwnd, hdc);
	DestroyIcon(fileInfo.hIcon);

	dragImage.sizeDragImage = size;
	dragImage.ptOffset = pt;
	dragImage.hbmpDragImage = hbmp;
	dragImage.crColorKey = RGB(0, 0, 0);
	hr = pDragSourceHelper->InitializeFromBitmap(&dragImage, pDataObject);

	return hr == S_OK;
}

void InsertListViewItem(HWND hwndListView)
{
	int             i = 0;
	TCHAR           szDirectoryName[256];
	HANDLE          hFindFile;
	WIN32_FIND_DATA findData;
	SHFILEINFO      fileInfo;
	LVITEM          item;
	
	GetCurrentDirectory(sizeof(szDirectoryName) / sizeof(TCHAR), szDirectoryName);
	lstrcat(szDirectoryName, TEXT("\\*"));

	hFindFile = FindFirstFile(szDirectoryName, &findData);

	do {
		if (lstrcmp(findData.cFileName, TEXT("..")) != 0 && lstrcmp(findData.cFileName, TEXT(".")) != 0) {
			SHGetFileInfo(findData.cFileName, 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_SYSICONINDEX);

			item.mask     = LVIF_TEXT | LVIF_IMAGE;
			item.iItem    = i;
			item.iSubItem = 0;
			item.pszText  = findData.cFileName;
			item.iImage   = fileInfo.iIcon;
			ListView_InsertItem(hwndListView, &item);

			i++;
		}

	} while(FindNextFile(hFindFile, &findData));
	
	FindClose(hFindFile);
}


// CDropSource


CDropSource::CDropSource()
{
	m_cRef = 1;
}

CDropSource::~CDropSource()
{
}

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

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IDropSource))
		*ppvObject = static_cast<IDropSource *>(this);
	else
		return E_NOINTERFACE;

	AddRef();

	return S_OK;
}

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

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

	return m_cRef;
}

STDMETHODIMP CDropSource::QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState)
{
	if (fEscapePressed)
		return DRAGDROP_S_CANCEL;

	if ((grfKeyState & MK_LBUTTON) == 0)
		return DRAGDROP_S_DROP;

	return S_OK;
}

STDMETHODIMP CDropSource::GiveFeedback(DWORD dwEffect)
{
	return DRAGDROP_S_USEDEFAULTCURSORS;
}


// CDropTarget


CDropTarget::CDropTarget(HWND hwnd)
{
	m_cRef = 1;
	m_hwnd = hwnd;

	CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pDropTargetHelper));
}

CDropTarget::~CDropTarget()
{
	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->Release();
}

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

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IDropTarget))
		*ppvObject = static_cast<IDropTarget *>(this);
	else
		return E_NOINTERFACE;

	AddRef();

	return S_OK;
}

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

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

	return m_cRef;
}

STDMETHODIMP CDropTarget::DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
	HRESULT   hr;
	FORMATETC formatetc;
	
	*pdwEffect = GetEffectFromKeyState(grfKeyState);
	
	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->DragEnter(m_hwnd, pDataObj, (LPPOINT)&pt, *pdwEffect);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST);
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	hr = pDataObj->QueryGetData(&formatetc);
	if (FAILED(hr)) {
		m_bSupportFormat = FALSE;
		*pdwEffect = DROPEFFECT_NONE;
	}
	else
		m_bSupportFormat = TRUE;

	m_bRButton = grfKeyState & MK_RBUTTON;

	return S_OK; 
}

STDMETHODIMP CDropTarget::DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
	*pdwEffect = GetEffectFromKeyState(grfKeyState);
	
	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->DragOver((LPPOINT)&pt, *pdwEffect);

	if (!m_bSupportFormat)
		*pdwEffect = DROPEFFECT_NONE;

	return S_OK;
}

STDMETHODIMP CDropTarget::DragLeave()
{
	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->DragLeave();

	return S_OK;
}

STDMETHODIMP CDropTarget::Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
	HRESULT          hr;
	UINT             i;
	LPTSTR           lpszFileName;
	TCHAR            szSrcFilePath[256];
	TCHAR            szDestFilePath[256];
	FORMATETC        formatetc;
	LPIDA            lpIda; 
	STGMEDIUM        medium;
	PIDLIST_ABSOLUTE pidl;
	DWORD            dwEffect = *pdwEffect;
	
	*pdwEffect = GetEffectFromKeyState(grfKeyState);

	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->Drop(pDataObj, (LPPOINT)&pt, *pdwEffect);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST);
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	hr = pDataObj->GetData(&formatetc, &medium);
	if (FAILED(hr)) {
		*pdwEffect = DROPEFFECT_NONE;
		return E_FAIL;
	}

	if (m_bRButton) {
		int   nId;
		HMENU hmenuPopup;

		hmenuPopup = CreatePopupMenu();
		
		if (dwEffect & DROPEFFECT_MOVE)
			InitializeMenuItem(hmenuPopup, TEXT("ここに移動"), DROPEFFECT_MOVE);
		if (dwEffect & DROPEFFECT_COPY)
			InitializeMenuItem(hmenuPopup, TEXT("ここにコピー"), DROPEFFECT_COPY);
		if (dwEffect & DROPEFFECT_LINK)
			InitializeMenuItem(hmenuPopup, TEXT("ここにリンク"), DROPEFFECT_LINK);

		InitializeMenuItem(hmenuPopup, NULL, 100);
		InitializeMenuItem(hmenuPopup, TEXT("キャンセル"), 0);

		nId = TrackPopupMenu(hmenuPopup, TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, NULL);
		if (nId == 0) {
			DestroyMenu(hmenuPopup);
			*pdwEffect = DROPEFFECT_NONE;
			return E_FAIL;
		}

		*pdwEffect = nId;
		DestroyMenu(hmenuPopup);
	}

	lpIda = (LPIDA)GlobalLock(medium.hGlobal);
	
	for (i = 0; i < lpIda->cidl; i++) {
		pidl = (PIDLIST_ABSOLUTE)(((LPBYTE)lpIda)+lpIda->aoffset[1 + i]);
		SHGetPathFromIDList(pidl, szSrcFilePath);

		lpszFileName = PathFindFileName(szSrcFilePath);
		lstrcpy(szDestFilePath, g_szBaseDirectory);
		PathAppend(szDestFilePath, lpszFileName);

		if (*pdwEffect == DROPEFFECT_MOVE)
			MoveFile(szSrcFilePath, szDestFilePath);
		else if (*pdwEffect == DROPEFFECT_COPY)
			CopyFile(szSrcFilePath, szDestFilePath, TRUE);
		else if (*pdwEffect == DROPEFFECT_LINK) {
			IShellLink   *pShellLink;
			IPersistFile *pPersistFile;
			WCHAR        szLinkPath[256];
			WCHAR        szUnicode[256];
			LPWSTR       lpsz;
			
			CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
			pShellLink->SetPath(szSrcFilePath);
			pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
#ifdef UNICODE
			lpsz = szDestFilePath;
#else
			MultiByteToWideChar(CP_ACP, 0, szDestFilePath, -1, szUnicode, MAX_PATH);
			lpsz = szUnicode;
#endif
			wsprintfW(szLinkPath, L"%s.lnk", lpsz);
			pPersistFile->Save(szLinkPath, TRUE);
			pPersistFile->Release();
			pShellLink->Release();
		}
		else
			;
	}
	
	GlobalUnlock(medium.hGlobal);
	ReleaseStgMedium(&medium);
	
	return S_OK;
}

DWORD CDropTarget::GetEffectFromKeyState(DWORD grfKeyState)
{
	DWORD dwEffect;

	if (grfKeyState & MK_CONTROL) {
		if (grfKeyState & MK_SHIFT)
			dwEffect = DROPEFFECT_LINK;
		else
			dwEffect = DROPEFFECT_COPY;
	}
	else
		dwEffect = DROPEFFECT_MOVE;

	return dwEffect;
}

void CDropTarget::InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId)
{
	MENUITEMINFO mii;
	
	mii.cbSize = sizeof(MENUITEMINFO);
	mii.fMask  = MIIM_ID | MIIM_TYPE;
	mii.wID    = nId;

	if (lpszItemName != NULL) {
		mii.fType      = MFT_STRING;
		mii.dwTypeData = lpszItemName;
	}
	else
		mii.fType = MFT_SEPARATOR;

	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

今回はドラッグとドロップの両方をサポートするということで、 IDropTargetを実装したオブジェクトとIDropSourceを実装したオブジェクトを作成しています。 また、ドラッグ中のデータのイメージを表示するために、 IDropTargetHelperとIDragSourceHelperも作成しています。 ただし、IDropTargetHelperはCDropTargetの中だけで必要になるため、クラス内で作成しています。 リストビューを作成してシステムイメージリストを設定したら、 InsertListViewItemという自作関数でカレントディレクトリの中身をリストビューに追加します。 これが終われば、SHChangeNotifyRegisterを呼び出してファイルの作成や削除を監視するようにします。 これは、カレントディレクトの中身とリストビューの内容が矛盾しないために必要です。 g_szBaseDirectoryはリストビューで表示するディレクトリであり、 今回の場合はカレントディレクトリになります。

今回のアプリケーションがDoDragDropを呼び出すことになるのは、 リストビューのアイテムをドラッグしようとしたときです。 この際には、通知コードがLVN_BEGINDRAGであるWM_NOTIFYが送られます。

if (((LPNMHDR)lParam)->code == LVN_BEGINDRAG) {
	int              i, j;
	int              nCount, nSelectedCount;
	TCHAR            szText[256];
	TCHAR            szFilePath[256];
	PITEMID_CHILD    *ppidlChild;
	PIDLIST_ABSOLUTE *ppidlAbsolute;
	IDataObject      *pDataObject;
	IShellFolder     *pShellFolder = NULL;
	DWORD            dwEffect;
	
	nSelectedCount = ListView_GetSelectedCount(hwndListView);
	if (nSelectedCount == 0)
		return 0;

	ppidlAbsolute = (PIDLIST_ABSOLUTE *)CoTaskMemAlloc(sizeof(PIDLIST_ABSOLUTE) * nSelectedCount);
	ppidlChild = (PITEMID_CHILD *)CoTaskMemAlloc(sizeof(PITEMID_CHILD) * nSelectedCount);

	nCount = ListView_GetItemCount(hwndListView);
	for (i = 0, j = 0; i < nCount; i++) {
		if (ListView_GetItemState(hwndListView, i, LVIS_SELECTED)) {
			ListView_GetItemText(hwndListView, i, 0, szText, sizeof(szText) / sizeof(TCHAR));
			wsprintf(szFilePath, TEXT("%s\\%s"), g_szBaseDirectory, szText);
			ppidlAbsolute[j++] = ILCreateFromPath(szFilePath);
		}
	}
	
	SHBindToParent(ppidlAbsolute[0], IID_PPV_ARGS(&pShellFolder), NULL);

	for (i = 0; i < nSelectedCount; i++)
		ppidlChild[i] = ILFindLastID(ppidlAbsolute[i]);
	pShellFolder->GetUIObjectOf(NULL, nSelectedCount, (LPCITEMIDLIST *)ppidlChild, IID_IDataObject, NULL, (void **)&pDataObject);
	
	SetDragImage(hwnd, ppidlAbsolute, pDragSourceHelper, pDataObject);
	DoDragDrop(pDataObject, pDropSource, DROPEFFECT_MOVE | DROPEFFECT_COPY | DROPEFFECT_LINK, &dwEffect);

	for (i = 0; i < nSelectedCount; i++)
		CoTaskMemFree(ppidlAbsolute[i]);
	CoTaskMemFree(ppidlAbsolute);
	CoTaskMemFree(ppidlChild);
}

選択されたアイテムをドラッグするということは、 その選択されたアイテムを表すIDataObjectを取得しなければなりません。 IDataObjectを取得するには、これに設定するアイテムのPIDLが必要になるため、 まずは現在選択されているアイテムを確認しなければなりません。 ListView_GetSelectedCountが0より大きい値を返すということは、 何らかのアイテムが選択されているということであり、 どのアイテムが選択されているかはListView_GetItemStateで確認することができます。 選択されているアイテムが見つかったら、ListView_GetItemTextでアイテムのテキストを取得し、 これとカレントディレクトリを連結して絶対形式のPIDLを作成します。 そして、これを事前に作成しておいたPIDLの配列に格納します。 選択されたアイテムを確認したら、親フォルダのIShellFolderを取得するためにSHBindToParentを呼び出します。 後はGetUIObjectOfを呼び出すことでIDataObjectを取得できますが、 これに指定するPIDLの配列は親フォルダの子のPIDLでなければならないため、 ppidlAbsoluteではなくppidlChildを指定しています。 IDataObjectを取得すればDoDragDropを呼び出すことができますが、 今回はその前にドラッグするイメージの設定を行っています。 ここで、IDragSourceHelperが使用されることになります。 DoDragDropの第3引数には、上記の3つの定数を指定するべきといえます。 DROPEFFECT_MOVEだけの場合は常にドロップが移動として扱われますが、 DROPEFFECT_COPYを指定していれば、Ctrlキーを押すことでコピーが可能になります。

SetDragImageの実装は次のようになっています。

BOOL SetDragImage(HWND hwnd, PIDLIST_ABSOLUTE *ppidlAbsolute, IDragSourceHelper *pDragSourceHelper, IDataObject *pDataObject)
{
	HRESULT     hr;
	HDC         hdc, hdcMem;
	HBITMAP     hbmp, hbmpPrev;
	SHFILEINFO  fileInfo;
	SHDRAGIMAGE dragImage;
	POINT       pt = {0, 0};
	SIZE        size = {32, 32};

	SHGetFileInfo((LPCTSTR)ppidlAbsolute[0], 0, &fileInfo, sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_ICON | SHGFI_LARGEICON);

	hdc = GetDC(hwnd);
	hdcMem = CreateCompatibleDC(hdc);
	hbmp = CreateCompatibleBitmap(hdc, size.cx, size.cy);
	hbmpPrev = (HBITMAP)SelectObject(hdcMem, hbmp);
	DrawIcon(hdcMem, 0, 0, fileInfo.hIcon);
	SelectObject(hdcMem, hbmpPrev);
	DeleteDC(hdcMem);
	ReleaseDC(hwnd, hdc);
	DestroyIcon(fileInfo.hIcon);

	dragImage.sizeDragImage = size;
	dragImage.ptOffset = pt;
	dragImage.hbmpDragImage = hbmp;
	dragImage.crColorKey = RGB(0, 0, 0);
	hr = pDragSourceHelper->InitializeFromBitmap(&dragImage, pDataObject);

	return hr == S_OK;
}

ドラッグするデータにイメージを設定するには、IDragSourceHelper::InitializeFromBitmapを呼び出す必要があります。 このメソッドはSHDRAGIMAGE構造体を要求し、hbmpDragImageにはイメージのビットマップを指定しておかなければなりません。 リストビューのアイテムから直接ビットマップハンドルを取得することはできないため、 まずはアイテムのPIDLを基にSHGetFileInfoを呼び出してアイコンのハンドルを取得します。 そして、次にメモリデバイスコンテキストとビットマップを作成し、 このビットマップをメモリデバイスコンテキストに割り当てます。 この状態でDrawIconを呼び出せば描画内容がビットマップに反映されるため、 これをhbmpDragImageメンバに指定できるようになります。 今回の実装では、PIDLの配列の中でppidlAbsolute[0]しか考慮していないため、 複数のファイルをドラッグする場合でもアイコンは1つだけ表示されることになります。

続いて、データを受け取る際の処理を見ていきます。 何らかのデータをドラッグした状態で、カーソルが今回のアプリケーションのウインドウに入った場合は、 IDropTarget::DragEnterが呼ばれます。

STDMETHODIMP CDropTarget::DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
	HRESULT   hr;
	FORMATETC formatetc;
	
	*pdwEffect = GetEffectFromKeyState(grfKeyState);
	
	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->DragEnter(m_hwnd, pDataObj, (LPPOINT)&pt, *pdwEffect);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST);
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	hr = pDataObj->QueryGetData(&formatetc);
	if (FAILED(hr)) {
		m_bSupportFormat = FALSE;
		*pdwEffect = DROPEFFECT_NONE;
	}
	else
		m_bSupportFormat = TRUE;

	m_bRButton = grfKeyState & MK_RBUTTON;

	return S_OK; 
}

pdwEffectにはカーソルの形を表すエフェクトを返すことになりますが、 これは現在のキーの状態で決定されることになります。 特にキーが押されてない場合は移動を示すDROPEFFECT_MOVEで構いませんが、 Ctrlキーが押されている場合はコピーを示すDROPEFFECT_COPYになり、 さらにShiftキーが押されている場合はリンクを示すDROPEFFECT_LINKでなければなりません。 GetEffectFromKeyStateは、そのための処理を行う自作関数です。 IDropTargetHelperを取得できている場合は、現在のメソッドと同一の名前のメソッドを呼び出すようにします。 これにより、データのイメージが表示されるようになります。 QueryGetDataを呼び出しているのは、ドロップされようとしているデータをサポートできるかどうかを確認するためです。 今回のアプリケーションはシェル上のファイルなどを受け取りたいわけですが、 そうしたデータはフォーマットとしてCF_DROPやCFSTR_SHELLIDLISTを含んでいるため、 これを基に確認すればよいでしょう。 m_bRButtonは、現在のドラッグがマウスの右ボタンで行われているかを格納します。

データがウインドウ上でドロップされた場合は、IDropTarget::Dropが呼ばれます。 ここでは、ファイルの移動、コピー、リンクのいずれかを行います。

STDMETHODIMP CDropTarget::Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
	HRESULT          hr;
	UINT             i;
	LPTSTR           lpszFileName;
	TCHAR            szSrcFilePath[256];
	TCHAR            szDestFilePath[256];
	FORMATETC        formatetc;
	LPIDA            lpIda; 
	STGMEDIUM        medium;
	PIDLIST_ABSOLUTE pidl;
	DWORD            dwEffect = *pdwEffect;
	
	*pdwEffect = GetEffectFromKeyState(grfKeyState);

	if (m_pDropTargetHelper != NULL)
		m_pDropTargetHelper->Drop(pDataObj, (LPPOINT)&pt, *pdwEffect);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST);
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	hr = pDataObj->GetData(&formatetc, &medium);
	if (FAILED(hr)) {
		*pdwEffect = DROPEFFECT_NONE;
		return E_FAIL;
	}

	if (m_bRButton) {
		int   nId;
		HMENU hmenuPopup;

		hmenuPopup = CreatePopupMenu();
		
		if (dwEffect & DROPEFFECT_MOVE)
			InitializeMenuItem(hmenuPopup, TEXT("ここに移動"), DROPEFFECT_MOVE);
		if (dwEffect & DROPEFFECT_COPY)
			InitializeMenuItem(hmenuPopup, TEXT("ここにコピー"), DROPEFFECT_COPY);
		if (dwEffect & DROPEFFECT_LINK)
			InitializeMenuItem(hmenuPopup, TEXT("ここにリンク"), DROPEFFECT_LINK);

		InitializeMenuItem(hmenuPopup, NULL, 100);
		InitializeMenuItem(hmenuPopup, TEXT("キャンセル"), 0);

		nId = TrackPopupMenu(hmenuPopup, TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, NULL);
		if (nId == 0) {
			DestroyMenu(hmenuPopup);
			*pdwEffect = DROPEFFECT_NONE;
			return E_FAIL;
		}

		*pdwEffect = nId;
		DestroyMenu(hmenuPopup);
	}

	lpIda = (LPIDA)GlobalLock(medium.hGlobal);
	
	for (i = 0; i < lpIda->cidl; i++) {
		pidl = (PIDLIST_ABSOLUTE)(((LPBYTE)lpIda)+lpIda->aoffset[1 + i]);
		SHGetPathFromIDList(pidl, szSrcFilePath);

		lpszFileName = PathFindFileName(szSrcFilePath);
		lstrcpy(szDestFilePath, g_szBaseDirectory);
		PathAppend(szDestFilePath, lpszFileName);

		if (*pdwEffect == DROPEFFECT_MOVE)
			MoveFile(szSrcFilePath, szDestFilePath);
		else if (*pdwEffect == DROPEFFECT_COPY)
			CopyFile(szSrcFilePath, szDestFilePath, TRUE);
		else if (*pdwEffect == DROPEFFECT_LINK) {
			IShellLink   *pShellLink;
			IPersistFile *pPersistFile;
			WCHAR        szLinkPath[256];
			WCHAR        szUnicode[256];
			LPWSTR       lpsz;
			
			CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
			pShellLink->SetPath(szSrcFilePath);
			pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
#ifdef UNICODE
			lpsz = szDestFilePath;
#else
			MultiByteToWideChar(CP_ACP, 0, szDestFilePath, -1, szUnicode, MAX_PATH);
			lpsz = szUnicode;
#endif
			wsprintfW(szLinkPath, L"%s.lnk", lpsz);
			pPersistFile->Save(szLinkPath, TRUE);
			pPersistFile->Release();
			pShellLink->Release();
		}
		else
			;
	}
	
	GlobalUnlock(medium.hGlobal);
	ReleaseStgMedium(&medium);
	
	return S_OK;
}

ドロップされたファイルの情報を取得するために、CFSTR_SHELLIDLISTをフォーマットとしてGetDataを呼び出します。 次に、ファイルを移動するかコピーするかなどを決定することになりますが、 マウスの左ボタンによるドラッグの場合は現在のキーの状態(GetEffectFromKeyStateの戻り値)を考慮して構いません。 しかし、マウスの右ボタンによるドラッグだった場合は、ポップアップメニューで操作を選択することになります。 TrackPopupMenuの第2引数にTPM_RETURNCMDを指定していることから、 選択した項目の通知はWM_COMMANDとしてm_hwndに送られず、戻り値として項目のIDが返ることになります。 CFSTR_SHELLIDLISTのフォーマットはデータをPIDLの配列で表しており、 ((LPBYTE)lpIda)+lpIda->aoffset[1]を参照すれば、ドロップされたファイルのPIDLを参照することができます。 ちなみに、Windows VistaであればSHGetIDListFromObjectにIDataObjectを指定するだけで、PIDLを取得することができます。 PIDLを取得したら、SHGetPathFromIDListでPIDLに関連するファイルパスを取得できるため、 これでドロップされたファイルは特定できたことになります。 後は、これを移動するかコピーするかなどを決めなければなりませんが、 その前にターゲットのパスをszDestFilePathとして用意することにしています。 これは、元となるファイルの名前をカレントディレクトリのパスに連結して作成すればよいでしょう。 pdwEffectがDROPEFFECT_MOVEである場合は、ファイルを移動したいということなのでMoveFileを呼び出し、 DROPEFFECT_COPYの場合はファイルをコピーしたいことなのでCopyFileを呼び出します。 また、DROPEFFECT_LINKの場合はショートカットを作成したいということなので、 IShellLinkを実装するオブジェクトを作成し、これのSetPathを呼び出すことでリンク先を設定します。 そして、IPersistFile::Saveを呼び出せば実際にショートカットファイルを作成することができます。 IPersistFile::SaveはファイルパスをUNICODE文字列で受け取るため、 UNICODEとしてコンパイルされていない場合は変換処理が必要になります。

IDragSourceHelper2について

実際にIDropTargetHelperを使用して分かったことですが、 このインターフェースで表示されるイメージには、 ドラッグ対象のアイテムのテキストが表示されません。 こうしたテキストを表示する機能かどうかは分かりませんが、 Windows Vistaから登場したIDragSourceHelper2にはドロップ情報を表示するためのメソッドが含まれているため、 これを使用する方法を考えてみます。

BOOL SerDropDescription(IDragSourceHelper *pDragSourceHelper, IDataObject *pDataObject)
{
	HRESULT            hr;
	LPVOID             lp;
	HGLOBAL            hglobal;
	STGMEDIUM          medium;
	FORMATETC          formatetc;
	DROPDESCRIPTION    dropDescription;
	IDragSourceHelper2 *pDragSourceHelper2;

	pDragSourceHelper->QueryInterface(IID_PPV_ARGS(&pDragSourceHelper2));
	if (pDragSourceHelper2 == NULL)
		return FALSE;
	pDragSourceHelper2->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT);
	pDragSourceHelper2->Release();
	
	dropDescription.type = DROPIMAGE_MOVE;
	lstrcpyW(dropDescription.szMessage, L"Move to %1");
	lstrcpyW(dropDescription.szInsert, L"Documents");

	hglobal = GlobalAlloc(GPTR, sizeof(DROPDESCRIPTION));
	lp = GlobalLock(hglobal);
	CopyMemory(lp, &dropDescription, sizeof(DROPDESCRIPTION));
	GlobalUnlock(hglobal);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_DROPDESCRIPTION);
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	medium.tymed          = TYMED_HGLOBAL;
	medium.hGlobal        = hglobal;
	medium.pUnkForRelease = NULL;
	
	hr = pDataObject->SetData(&formatetc, &medium, TRUE);

	return hr == S_OK;
}

IDragSourceHelper2は、IDragSourceHelperのQueryInterfaceを呼び出すことで取得できます。 SetFlagsにDSH_ALLOWDROPDESCRIPTIONTEXTを指定することでドロップ情報が表示されるようになるようですが、 このためにはそのドロップ情報をIDataObject::SetDataに設定しておく必要があるはずです。 よって、フォーマットとしてCFSTR_DROPDESCRIPTIONを指定し、 データとしてDROPDESCRIPTION構造体を用意したうえで、 IDataObject::SetDataを呼び出すことになります。 ただ、このDROPDESCRIPTION構造体には、具体的にどのような値を指定すればよいのかよく分かりません。 typeメンバにDROPIMAGE_MOVEを指定した場合は、szMessageメンバに"Move to %1"のような文字列を指定するべきであるようですが、 これが何の意味を持つのかはよく分かりません。 また、szInsertメンバについても同じよく分かりません。

実際に、上記の関数をIDragSourceHelper::InitializeFromBitmapの前に呼び出してみたところ、 ドロップ情報がイメージと共に表示されることは確認できませんでした。 そもそも、IDataObject::SetDataを呼び出すのがドロップソースではなくドロップターゲットになっているサンプルも見かけたことがあり、 どのような処理が適切であるのか分からないのが現状です。



戻る