Tips in Writing Namespace Extension (III) - Drag and Drop objects between System Namespace and your NSE






4.73/5 (19 votes)
Feb 23, 2006
17 min read

246215

2070
An article on implementing drag and drop operation between your NSE and system namespace.
Contents
- Introduction
- About drag and drop
- Initialize the COM Library
- Implement drag and drop relative shell interfaces
- Conclusion
Introduction
To implement drag and drop (abbr. D&D) operation between Shell Namespace Extension (abbr. NSE) and Explorer is a little bit more complicated than implementing it between common Windows-based applications and Explorer. As we known, NSE includes two parts in UI, Tree View and Shell View. To implement D&D in Shell View is similar in common applications, but as for Tree View, you need some other skills. To support D&D operation in NSE involves implementing shell interfaces including IDataObject
, IDropTarget
, IDropSource
, IEnumFORMATETC
and you should carefully handle those interfaces while implement IShellFolder
and IShellView
.
Another thing I must point out is --- don't mix D&D up with another shell technology drag-and-drop handler. The biggest difference in UI between the two things is that D&D operation normally happens with the left mouse button, while drag-and-drop handler is a shortcut menu handler called when a file is dragged with the right mouse button.
Before reading this article, I recommend you read my first article about NSE Tips in Writing Namespace Extension (I) - Implements Subfolder -- which introduces how our sample NSE's data be organized in detail (I mean to make all my articles about NSE based on the same kind of NSE data). This article assumes you know C++, ATL, COM and are especially familiar with basic knowledge of NSE.
The sample project is created using ATL COM AppWizard. Your can freely drag and drop files between Explorer and sample NSE or just within sample NSE. For simplicity, we don't support a folder as the dragged data. However, the key elements about how to implement drag and drop are the same.
About Drag and Drop
The D&D operation involves four participants to take care of the whole process:
- Drag source: Implemented by the object where the data will be dragged from, which should expose an
IDropSource
interface. - Data object: Implemented by the object that contains the data being dragged, which should expose an
IDataObject
interface. - Drop target: Implemented by the object that is intended to accept the drop, which should expose an
IDropTarget
interface. DoDragDrop
: The function implemented by OLE and used to initiate a drag and drop operation. Once the operation is in progress, it facilitates communication between the drag source and the drop target.
For example, if you drag a file from Explorer to our NSE, the Explorer is the drag source and is responsible for preparing the data object and initiating the DoDragDrop
function, NSE is the drop target and is responsible for handling the dropped data object.
What happened during drag and drop
The following procedure outlines the essential steps that are typically used to transfer data with drag and drop (quote from MSDN):
- The target calls
RegisterDragDrop
to give the system a pointer to itsIDropTarget
interface and register a window as a drop target. - When the user starts a drag-and-drop operation, the source creates a data object and initiates a "drag loop" by calling
DoDragDrop
. - When the cursor is over the target window, the system notifies the target by calling one of the target's
IDropTarget
methods. The system callsIDropTarget::DragEnter
when the cursor enters the target window, andIDropTarget::DragOver
as the cursor passes over the target window. Both methods provide the drop target with the current cursor position and the state of the keyboard modifier keys such as CTRL or ALT. When the cursor leaves the target window, the system notifies the target by callingIDropTarget::DragLeave
. When any of these methods return, the system calls theIDropSource
interface to pass the return value to the source. - When the user releases the mouse button to drop the data, the system calls the target's
IDropTarget::Drop
method. Among the method's parameters is a pointer to the data object'sIDataObject
interface. - The target calls the data object's
IDataObject::GetData
method to extract the data. - With some Shell data transfers, the target might also need to call the data object's
IDataObject::SetData
method to provide feedback to the source on the outcome of the data transfer. - When the target is finished with the data object, it returns from
IDropTarget::Drop
. The system returns the source'sDoDragDrop
call to notify the source that the data transfer is complete. - Depending on the particular data transfer scenario, the source might need to take additional action based on the value returned by
DoDragDrop
and the values that are passed to the data object by the target. For instance, when a file is moved, the source must check these values to determine whether it must delete the original file. - The source releases the data object.
Implement drop in Tree View and Shell View
When the user drops an object into NSE's Tree View:
- If the target is NSE's subfolder, Explorer will call
IShellFolder::GetUIObjectOf
method with theriid
parameter set toIID_IDropTarget
to create a drop target object and get theIDropTarget
interface pointer back to handle the drag-and-drop operation. - If the target is NSE's root folder,
IShellFolder::CreateViewObject
will be called by Explorer instead ofIShellFodler::GetUIObjectOf
to require anIDropTraget
interface pointer. - Meanwhile, you should set the corresponding attribute to subfolder by add
SFGAO_DROPTARGET
attribute to folder object when Explorer callsIShellFolder::GetAttributesOf
to retrieve the subfolder's feature. As for the root folder, you should addSFGAO_DROPTARGET
attribute to "Attributes
" value in "ShellFolder
" key when registering the NSE.
To support drop on Shell View, you should:
- Register the
IDropTarget
interface pointer of target object with the Shell View's window which will accept dropped data byRegisterDragDrop
.
Implement drag in Tree View and Shell View
When the user drags an object from NSE's Tree View:
- Explorer will call
IShellFolder::GetUIObjectOf
method and set theriid
parameter toIID_IDataObject
to create a data object and get theIDataObject
interface pointer to handle the drag-and-drop operation.
To support dragging an object from NSE's Shell View:
- When handling the
LVN_BEGINDRAG
notify triggered by drag-and-drop operation, you should create a data object which implements theIDataObject
interface together with a freshly created source object which supports theIDropSource
interface, and use those two interface pointers to initiate drag-and-drop operation by callingDoDragDrop
function.
Data object: data to be transferred during Drag and Drop
As we known, data object is the object holding the data that is dragged from the drag source to the drop target. What data will be held by the data object is totally decided by your application's requirement. But there are still some rules about how the data will be organized.
Which kind of data that data object will hold is identified by the clipboard formats data object supported. And each clipboard format has a corresponding structure which specifies the way in which dragged data will be organized in its storage medium. There are some system registered clipboard formats and users can also register their own private clipboard formats for their applications. For system clipboard formats, the system has predefined the corresponding structure. As for the application's private clipboard format, it is the application designer who is responsible for defining how data can be organized.
Normally, the data object will support several clipboard formats simultaneously.
The FORMATETC
structure is used by methods in the D&D to specifying which kind of data can be requested or be interested in, besides clipboard format. It also includes other information about the data such as type of storage medium, and target device to use, etc.
The STGMEDIUM
structure is used to specify the data's real storage medium.
typedef struct tagFORMATETC
{
CLIPFORMAT cfFormat; //Particular clipboard format of interest
DVTARGETDEVICE *ptd;
DWORD dwAspect;
//Constants that indicate how much detail should be
//contained in the rendering
LONG lindex;
DWORD tymed;
//Type of storage medium used to transfer
//the object's data
}FORMATETC, *LPFORMATETC;
typedef struct tagSTGMEDIUM
{
DWORD tymed; //Type of storage medium
union {
HBITMAP hBitmap;
HMETAFILEPICT hMetaFilePict;
HENHMETAFILE hEnhMetaFile;
HGLOBAL hGlobal; //Global memory handle.
//Address of the data be transferred.
LPWSTR lpszFileName;
IStream *pstm;
IStorage *pstg;
};
IUnknown *pUnkForRelease;
}STGMEDIUM;
The shell itself always uses the tymed
of TYMED_HGLOBAL
together with the dwAspect
set to DVASPECT_CONTENT
, which means it uses the global memory as the dragged data's storage medium.
Clipboard Formats and Drop Effects
CF_HDROP
The most commonly used system clipboard format in D&D is CF_HDROP
.
The data correspond to CF_HDROP
consists of an STGMEDIUM
structure which contains a global memory object. The structure's hGlobal
member points to a DROPFILES
structure as its hGlobal
member. And the pFiles
member of the DROPFILES
structure contains an offset to a double NULL
-terminated character array containing the dragged files' names.
typedef struct _DROPFILES {
DWORD pFiles;
POINT pt;
BOOL fNC;
BOOL fWide;
} DROPFILES, FAR * LPDROPFILES;
When dragging files from Explorer, Explorer will prepare a data object which supports CF_HDROP
clipboard format. Explorer will also try to acquire the data object by using the CF_HDROP
format to retrieve the data when Explorer receives a data object.
So, as long as the drop target and data object are implemented in our NSE support CF_HDROP
format, our NSE will accept the data dragged from Explorer and Explorer will accept the data dragged from our NSE.
The question is: since in our NSE, the file objects are all virtual files (i.e. there is no real file object mapped with the virtual file object), which filename will be used when preparing the DROPFILES
for the CF_HDROP
format? Our solution is to create a temporary file with the same name of virtual file, and use this temp file's information to fill the DROPFILES
structure.
Register your own Clipboard Format for NSE
Another situation is that sometimes D&D may happened inside our NSE. It is easy to image, if we can find out a way enable us to use the internal data structure to store data in storage medium when D&D is inside NSE instead of something like DROPFILES
, then when we handle the data object, the process will become more convenient because we spare ourselves the steps of packing the data into DROPFILES
and then unpacking back.
The key point is find out a way to distinguish the data object dragged from inside NSE from those dragged from outside. The solution is to register a private clipboard format and use the internal data structure to organize the data in a storage medium. When our NSE receives a data object, the first thing we do is to query the data object whether it supports the private clipboard format or not? If yes, then the data object is dragged within NSE, we can draw the data which stores use internal structure out and handles it directly.
I choose to declare the private clipboard format m_CFSTR_NSEDRAGDROP
as the data member of _Module
, a global instance of CComModule
which is automatically generated by ATL COM AppWizard when creating our NSE. For the source code of declare and register the clipboard format please refer here.
And the data stored in storage medium corresponding to the private clipboard format is the complex pidl
of the dragged file object.
Choose the appropriate Drop Effect
Drop effect defines the effects of a drag-and-drop operation. The System uses the drop effect to decide which cursor should be displayed when dragging over the target and how we handle the original data when D&D completes successfully.
Drop effects include:
DROPEFFECT_NONE
: Drop target cannot accept the data. Corresponding cursor isDROPEFFECT_COPY
: Drop results in a copy. The original data is untouched by the drag source. Corresponding cursor isDROPEFFECT_MOVE
: Drag source should remove the data. Corresponding cursor isDROPEFFECT_LINK
: The data will be "linked to" by the drop target. Corresponding cursor is
Drop effect will be returned by drop target methods to tell the system to use the correct cursor and notify the source of how to treat the original data. It will also be used by the drag source to specify which kind of drop effect will be supported in D&D operation.
In sample NSE, when D&D completes successfully:
- If the data is dragged from our NSE, either it drop inside NSE or into Explorer, we choose the effect:
DROPEFFECT_MOVE
, i.e., the original file to be dragged will be deleted. - If the data is dragged from Explorer and dropped into our NSE, we choose the effect:
DROPEFFECT_COPY
, for we don't want to make changes to the original file.
Certainly, you can choose the drop effect suitable for your own application.
Initialize the COM Library
Applications that use the drag and drop functionality must call OleInitialize
before calling any other function in the COM library.
So, we should call ::OleInitialize
when we init the COM module, and ::OleUninitialize
when the COM module will be terminated. Like this:
class CNWSModule : public CComModule
{
public:
HRESULT Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h, const GUID* plibid = NULL)
{
::OleInitialize(NULL);
.... //initialize other data members
// Register Self-defined clipformat
m_CFSTR_NSEDRAGDROP =
(CLIPFORMAT)::RegisterClipboardFormat(_T("NSEDRAGDROP"));
return CComModule::Init(p, h, plibid);
}
void Term()
{
.... //release the resource occupied by data members
::OleUninitialize();
CComModule::Term();
}
...... //other data members
CLIPFORMAT m_CFSTR_NSEDRAGDROP;
};
Implement Shell Interfaces
Note:The standard methods of interfaces which have not appeared in following description are treated as not implemented.
IDropTarget
If you want to develop a NSE without a feature of Tree View (that is, NSE without subfolder) and don't want the root of NSE to be the drop target, you can simply let the Shell View implement IDropTarget
itself. But if you take the Tree view into consideration, when you want to support drop operation in root folder and subfolders, which will also require an object to expose an IDropTarget
interface, an independent class CNSFDropTarget
to implement IDropTarget
interface is a better choice.
The advantage is that you can reuse the class in both situations. Furthermore, the source code becomes easier to maintain. You will see that all other interfaces (except IShellFolder
and IShellView
) relative to drag-and-drop implementation are be realized in independent classes for the same reason.
Self-defined data members
CMyVirtualFolder *m_pFolder; //current folder object
UINT m_iAcceptFmt; //format used to handle the dropped data object
LPITEMIDLIST m_pidl; //!= NULL: simple pidl of the folder which accepts
// drop object (relative to current folder)
//== NULL: indicate that data object dropped
//in current folder
CNWSPidlMgr m_PidlMgr; //pidl management object
Self-defined functions
HRESULT _Init(CMyVirtualFolder *pFolder, LPCITEMIDLIST pidl);
// initialize data members
DWORD _QueryDrop();//decide which kind of drop effect we should take
//according to the accepted clipboard format
DragEnter/DragOver/DragLeave/Drop
DragEnter()
: Called when the cursor enters your Window.DragOver()
: Called when the cursor moves inside your Window.DragLeave()
: Called when the cursor leaves your Window.Drop()
: Called when the user drops in your Window.
Explorer calls the DragEnter
method to make sure whether a drop can be accepted by the drop target, and, if so, the effect of the drop.
When implementing this method, we will call data object's QueryGetData
function twice to ask whether the data object supports the clipboard format we cared. Like this:
- First, does it support
_Module.m_CFSTR_NSEDRAGDROP
? If so, drop effect set toDROPEFFECT_MOVE
. - Else, does it support
CF_HDROP
? If so, drop effect isDROPEFFECT_COPY
. - Otherwise, drop effect is
DROPEFFECT_NONE
.
STDMETHOD(DragEnter)(LPDATAOBJECT pDataObj,
DWORD dwKeyState,
POINTL,
LPDWORD pdwEffect)
{
ATLASSERT(pDataObj);
m_iAcceptFmt = 0;
// Does the data object support private clipboard format?
FORMATETC fe2 = { _Module.m_CFSTR_NSEDRAGDROP, NULL,
DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
if( S_OK == pDataObj->QueryGetData(&fe2) )
m_iAcceptFmt = FMT_NSEDRAGDROP_INDEX;
else
{
FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
if( S_OK == pDataObj->QueryGetData(&fe) )
m_iAcceptFmt = FMT_HDROP_INDEX;
}
*pdwEffect = _QueryDrop();
return S_OK;
}
For DragEnter()
, we had decided on the drop effect, here we just need to reuse the result.
STDMETHOD(DragOver)(DWORD dwKeyState, POINTL /*pt*/, LPDWORD pdwEffect)
{
*pdwEffect = _QueryDrop();
return S_OK;
}
When the cursor leaves, we reset the m_iAcceptFmt
to 0
.
STDMETHOD(DragLeave)(VOID)
{
m_iAcceptFmt = 0;
return S_OK;
}
Drop()
is where the data object will finally be handled, you can deal with the dropped data object in whatever way makes sense for your app. In our NSE, the dropped files are added to the Shell View and filenames will been added to the target folder's corresponding section in configuration file.
We call IShellFolder::_DoDrop
to accomplish the task. Please refer to Handle the dropped data object to see how we realize it.
STDMETHOD(Drop)(LPDATAOBJECT pDataObj,
DWORD dwKeyState,
POINTL /*pt*/,
LPDWORD pdwEffect)
{
ATLASSERT(pDataObj);
ATLASSERT(pdwEffect);
ATLASSERT(m_pFolder);
*pdwEffect = DROPEFFECT_NONE; // default to failed/cancelled
// Determine drop effect...
DWORD dwDropEffect = _QueryDrop();
if( dwDropEffect == DROPEFFECT_NONE )
return S_OK;
// Drop data...
HRESULT Hr;
if( SUCCEEDED(Hr = m_pFolder->_DoDrop(pDataObj, dwDropEffect, m_pidl,
m_iAcceptFmt) ) )
{
*pdwEffect = dwDropEffect;
return S_OK;
}
else
return Hr;
}
IDropSource
Drag source must create an object that exposes an IDropSource
interface. This interface allows the source to update the drag image that indicates the current position of the cursor and to provide feedback to the system on how to terminate a drag-and-drop operation. IDropSource
has two methods: GiveFeedback
and QueryContinueDrag
.
QueryContinueDrag
: Determines whether a drag-and-drop operation should continue.GiveFeedback
: Gives visual feedback to an end user during a drag-and-drop operation.
The logic to determine where the drag-and-drop operation should continue or not in our implementation is simple:
- If the Esc key has been pressed since the previous call, the operation is cancelled by the user without dropping the data.
- If the left button of the mouse is up, the operation is completed with data being dropped.
- Otherwise, the drag operation will be continued.
STDMETHOD(QueryContinueDrag)(BOOL bEsc, DWORD dwKeyState)
{
if( bEsc )
return ResultFromScode(DRAGDROP_S_CANCEL);
if( (dwKeyState & MK_LBUTTON)==0 )
return ResultFromScode(DRAGDROP_S_DROP);
return S_OK;
}
While in the drag loop, a drop source is responsible for keeping track of the cursor position and displaying an appropriate drag image. Here, we let OLE to update the cursor for us, using its defaults.
STDMETHOD(GiveFeedback)(DWORD)
{
return ResultFromScode(DRAGDROP_S_USEDEFAULTCURSORS);
}
IDataObject
IDataObject
is a data object's primary interface. It must be implemented by all data objects and used by both source and target for a variety of purposes, including:
- Loading data into the data object.
- Extracting data from the data object.
- Determining what types of data are in the data object.
- Providing feedback to the data object on the outcome of the data transfer.
The data object dragged from our NSE only supports two kinds of data described in following FORMATETC
structures:
- Format 1:
{ _Module.m_CFSTR_NSEDRAGDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }
: Which will be used when transfering data inside our NSE. - Format 2:
{ CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }
: Used when transfering data from our NSE to Explorer.
Following data will be extracted from our data object according to the specified format:
- Format 1: The complex
pidl
of the dragged file. - Format 2: The absolute path of the file created in temporary folder with the same filename as the dragged file that can be packed into
DROPFILES
struct.
Self-defined data members
CMyVirtualFolder* m_pFolder; //folder which data object dragged from
LPITEMIDLIST m_pidl; //the complex pidl of selected file
CNWSPidlMgr m_PidlMgr; //pidl management object
Self-defined functions
HRESULT _Init(CMyVirtualFolder *pFolder, LPCITEMIDLIST pidl);
//initialize the data members
HRESULT _GetLMouseStatus(BOOL *bM_LButtonUp);
//Used to get the status of left button mouse
//which indicates whether the drag-and-drop loop ended or not.
Support this type of data or not?
Determines whether the data object is capable of rendering the data described in the FORMATETC
structure.
STDMETHOD(QueryGetData)(FORMATETC* pFormatetc)
{
ATLASSERT(pFormatetc);
if(pFormatetc == NULL)
return E_INVALIDARG;
if( pFormatetc->cfFormat != _Module.m_CFSTR_NSEDRAGDROP &&
pFormatetc->cfFormat != CF_HDROP )
return DV_E_FORMATETC;
if(pFormatetc->ptd!=NULL)
return E_INVALIDARG;
if( (pFormatetc->dwAspect & DVASPECT_CONTENT)==0 )
return DV_E_DVASPECT;
if(pFormatetc->lindex !=-1)
return DV_E_LINDEX;
if((pFormatetc->tymed & TYMED_HGLOBAL) ==0)
return DV_E_TYMED;
return S_OK;
}
Extract data from our data object
This function will be called by a data consumer to obtain data from a source data object. It renders the data described in the specified FORMATETC
structure and transfers it through the specified STGMEDIUM
structure.
Please refer here to see which FORMATETC
structure our NSE supports and what data will be filled in the specified STGMEDIUM
structure.
STDMETHOD(GetData)(FORMATETC *pFormatetc, STGMEDIUM *pMedium)
{
ATLASSERT(m_pFolder);
if(m_pFolder==NULL)
return E_FAIL;
VALIDATE_POINTER(pFormatetc);
VALIDATE_POINTER(pMedium);
HRESULT Hr;
::ZeroMemory(pMedium, sizeof(STGMEDIUM));
if( pFormatetc->cfFormat == _Module.m_CFSTR_NSEDRAGDROP )
//Drag-and-Drop inside NSE
{
//Transfer the PIDL
UINT dwSize = m_PidlMgr.GetByteSize(m_pidl);
//m_pidl is a complex pidl
HGLOBAL hMem = ::GlobalAlloc(GMEM_ZEROINIT| GMEM_MOVEABLE
| GMEM_SHARE | GMEM_DISCARDABLE, dwSize);
if( hMem==NULL )
return E_OUTOFMEMORY;
LPTSTR psz = (LPTSTR)::GlobalLock(hMem);
ATLASSERT(psz);
if ( NULL == psz )
{
::GlobalFree ( hMem );
return E_UNEXPECTED;
}
::CopyMemory(psz,m_pidl,dwSize);
::GlobalUnlock(hMem);
pMedium->tymed = TYMED_HGLOBAL;
pMedium->hGlobal = hMem;
pMedium->pUnkForRelease = NULL;
return S_OK;
}
else if( pFormatetc->cfFormat == CF_HDROP ) //drag from NSE to system NS
{
//Transfer the temporary create file name
BOOL bM_LButtonUp = FALSE;
//status of mouse left button (FALSE:down TRUE:up)
HR(_GetLMouseStatus(&bM_LButtonUp));
TCHAR szTmpFileName[MAX_PATH]=_T("");
//1 create a temporary file use the selected object's name
if( bM_LButtonUp == TRUE )
{
DWORD dwSize=MAX_PATH;
//1.1 get selected object's name
TCHAR szFileName[MAX_PATH]=_T("");
LPITEMIDLIST pidlTemp = m_PidlMgr.GetLastItem(m_pidl);
HR(m_PidlMgr.GetName(pidlTemp,szFileName));
if(dwSize == 0)
return E_FAIL;
//1.2 create a temporary file use the name
dwSize=MAX_PATH;
::GetTempPath(dwSize,szTmpFileName);
_tcscat(szTmpFileName,szFileName);
FILE* fpTmp = _tfopen(szTmpFileName,_T("a"));
if(fpTmp == NULL)
return E_FAIL;
fclose(fpTmp);
}
//Alloc memory for data: DropFile Struct + FileName string
//the data that follows is a double null-terminated list
//of file names.
HGLOBAL hgDrop = ::GlobalAlloc ( GPTR, sizeof(DROPFILES) +
sizeof(TCHAR) * (_tcslen(szTmpFileName)+1) );
if ( NULL == hgDrop )
return E_OUTOFMEMORY;
DROPFILES* pDrop = (DROPFILES*) ::GlobalLock ( hgDrop );
ATLASSERT(pDrop);
if ( NULL == pDrop )
{
::GlobalFree ( hgDrop );
return E_UNEXPECTED;
}
// Fill in the DROPFILES struct.
pDrop->pFiles = sizeof(DROPFILES);
#ifdef _UNICODE
// If we're compiling for Unicode,
// set the Unicode flag in the struct to
// indicate it contains Unicode strings.
pDrop->fWide = TRUE;
#endif
// Append FileName data after DROPFILES Structure
LPTSTR pszBuff = (LPTSTR) (LPBYTE(pDrop) + sizeof(DROPFILES));
_tcscpy( pszBuff, (LPCTSTR) szTmpFileName );
::GlobalUnlock ( hgDrop );
pMedium->tymed = TYMED_HGLOBAL;
pMedium->hGlobal = hgDrop;
pMedium->pUnkForRelease = NULL;
return S_OK;
}
return DV_E_FORMATETC;
}
Enumerate all supported clipboard formats
Creates an object for enumerating the FORMATETC
structures for our data object.
STDMETHOD(EnumFormatEtc)(DWORD /*dwDirection*/,
IEnumFORMATETC** ppenumFormatEtc)
{
VALIDATE_POINTER(ppenumFormatEtc);
HRESULT Hr;
CComObject<CNSFEnumFmtEtc> *obj;
HR( CComObject<CNSFEnumFmtEtc>::CreateInstance(&obj) );
obj->AddRef();
Hr=obj->QueryInterface(IID_IEnumFORMATETC, (LPVOID *)ppenumFormatEtc);
obj->Release();
return Hr;
}
IEnumFORMATETC
The IEnumFORMATETC
interface is used to enumerate an array of FORMATETC
structures. IEnumFORMATETC
has the same methods as all enumerator interfaces: Next
, Skip
, Reset
, and Clone
.
The order of formats enumerated through the IEnumFORMATETC
object should be the same as the order that the formats would be in when placed on the clipboard.
As we known, here we only support two formats.
STDMETHOD(Next)(ULONG, LPFORMATETC pFormatetc, ULONG *pdwCopied)
{
VALIDATE_POINTER(pFormatetc);
if( pdwCopied!=NULL )
*pdwCopied = 1L;
m_dwEnumPos++;
switch( m_dwEnumPos )
{
case 2:
pFormatetc->cfFormat = CF_HDROP;
pFormatetc->ptd = NULL;
pFormatetc->dwAspect = DVASPECT_CONTENT;
pFormatetc->lindex = -1;
pFormatetc->tymed = TYMED_HGLOBAL;
break;
case 1:
pFormatetc->cfFormat = _Module.m_CFSTR_NSEDRAGDROP;
pFormatetc->ptd = NULL;
pFormatetc->dwAspect = DVASPECT_CONTENT;
pFormatetc->lindex = -1;
pFormatetc->tymed = TYMED_HGLOBAL;
break;
default:
if( pdwCopied!=NULL )
*pdwCopied = 0L;
return S_FALSE;
}
return S_OK;
}
STDMETHOD(Skip)(ULONG n)
{
m_dwEnumPos += n;
return S_OK;
}
STDMETHOD(Reset)(void)
{
m_dwEnumPos = 0L;
return S_OK;
}
IShellFolder
Support drop on Tree View's subfolder
STDMETHODIMP CMyVirtualFolder::GetUIObjectOf(HWND hWnd,
UINT nCount,
LPCITEMIDLIST* pidls,
REFIID riid, LPUINT,
LPVOID* ppRetVal)
{
...... // support other interfaces like IContextMenu IExtractIcon etc
if( riid == IID_IDropTarget )
{
if( nCount !=1)
return E_FAIL;
CComObject<CNSFDropTarget>* pDropTarget;
HR( CComObject<CNSFDropTarget>::CreateInstance(&pDropTarget) );
pDropTarget->AddRef();
HR( pDropTarget->_Init(this, *pidls) );
Hr=pDropTarget->QueryInterface(IID_IDropTarget, ppRetVal);
pDropTarget->Release();
return Hr;
}
return E_NOINTERFACE;
}
Support drop on Tree View's root folder and IShellView
STDMETHODIMP CMyVirtualFolder::CreateViewObject(HWND hWnd,
REFIID riid,
LPVOID* ppRetVal)
{
VALIDATE_OUT_POINTER(ppRetVal);
HRESULT Hr;
...... // support other interface like IShellView etc
//1.Called by the IShellView to enable view object be the drop target
//2.If you want the root folder be the droptarget,
// you should support IDropTarget in this method
if( riid == IID_IDropTarget )
{
CComObject<CNSFDropTarget> *pDropTarget;
HR( CComObject<CNSFDropTarget>::CreateInstance(&pDropTarget) );
pDropTarget->AddRef();
HR( pDropTarget->_Init(this, NULL) );
Hr=pDropTarget->QueryInterface(IID_IDropTarget, ppRetVal);
pDropTarget->Release();
return Hr;
}
return E_NOINTERFACE;
}
Set correct attributes to subfolder object
STDMETHODIMP CMyVirtualFolder::GetAttributesOf(UINT uCount,
LPCITEMIDLIST pidls[],
LPDWORD pdwAttribs)
{
*pdwAttribs = (DWORD)-1;
if(( uCount==0 )||(pidls[0]->mkid.cb == 0))
{
// Can happen on Win95. Queries the view folder.
DWORD dwAttr;
dwAttr |= SFGAO_FOLDER | SFGAO_HASSUBFOLDER
|SFGAO_HASPROPSHEET |SFGAO_DROPTARGET;
*pdwAttribs &= dwAttr;
}
else
{
for( UINT i=0; i<uCount; i++ )
{
DWORD dwAttr = 0;
switch( m_PidlMgr.GetItemType(pidls[i]) )
{
case NWS_FOLDER:
.....
dwAttr |=SFGAO_DROPTARGET;
//enable the folder be the drop target
......
break;
}
*pdwAttribs &= dwAttr;
}
}
return S_OK;
}
Handle the dropped data object
In our NSE, the dropped files are added to the Shell View and filenames will been added to the target folder's corresponding section in configuration file.
If the data object is dragged from Explorer, we retrieve the filename of each dragged file from data object and add it into current listview in our Shell View.
If the data object is dragged from our NSE, we retrieve the filename of each dragged file from data object and add it into current listview in our Shell View and delete the dragged file object from our NSE.
HRESULT CMyVirtualFolder::_DoDrop(LPDATAOBJECT pDataObj,
DWORD dwDropEffect,
LPITEMIDLIST pidlDropDest,
UINT iFmtIdx)
{
VALIDATE_POINTER(pDataObj);
HRESULT Hr = E_INVALIDARG;
STGMEDIUM stgmed;
if(FMT_HDROP_INDEX == iFmtIdx)
{
// Check for HDROP
FORMATETC fe1 = { CF_HDROP, NULL, DVASPECT_CONTENT, -1,
TYMED_HGLOBAL };
if( SUCCEEDED( pDataObj->GetData(&fe1, &stgmed) ) )
{
Hr = _DoDrop_HDROP(stgmed.hGlobal, dwDropEffect,pidlDropDest);
::ReleaseStgMedium(&stgmed);
return Hr;
}
}
else if( FMT_NSEDRAGDROP_INDEX == iFmtIdx )
{
// Check for CFSTR_NEFILENAME
FORMATETC fe2 = { _Module.m_CFSTR_NSEDRAGDROP, NULL,
DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
if( SUCCEEDED( pDataObj->GetData(&fe2, &stgmed) ) )
{
Hr = _DoDrop_NSEDRAGDROP
(stgmed.hGlobal, dwDropEffect,pidlDropDest);
::ReleaseStgMedium(&stgmed);
return Hr;
}
}
return Hr;
}
HRESULT CMyVirtualFolder::_DoDrop_HDROP(HGLOBAL hMem,
DWORD dwDropEffect,
LPITEMIDLIST pidlDropDest)
{
VALIDATE_POINTER(hMem);
TCHAR szMsg[MAX_PATH]={0};
HRESULT Hr = S_OK;;
// Get target path
LPITEMIDLIST pidlComplexDest = m_PidlMgr.Concatenate
(m_pidlPath,pidlDropDest);
TCHAR szDropDest[MAX_PATH]=_T("");
DWORD dwSize=MAX_PATH;
if(pidlComplexDest) //Dest is not the root folder
{
HR(m_PidlMgr.GetFullName(pidlComplexDest,szDropDest,&dwSize));
if(dwSize == 0)
{
m_PidlMgr.Delete(pidlComplexDest);
return E_FAIL;
}
}
DWORD dwFileOpFlags = 0;
// Iterate over HDROP information
HDROP hDrop = (HDROP) ::GlobalLock(hMem);
if( hDrop==NULL )
{
m_PidlMgr.Delete(pidlComplexDest);
return E_OUTOFMEMORY;
}
TCHAR szItemName[MAX_PATH]={0};
PTCHAR pChr = NULL;
TCHAR szFileName[MAX_PATH]={0};
UINT nFiles = ::DragQueryFile(hDrop, (UINT) -1, NULL, 0);
//Check whether dragged object include folder?
for( UINT i=0; i<nFiles; i++ )
{
// Get dragged filename
if( ::DragQueryFile(hDrop, i, szFileName, MAX_PATH)==0 )
{
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return E_FAIL;
}
// dragged a folder
if((::GetFileAttributes(szFileName) & FILE_ATTRIBUTE_DIRECTORY)!=0)
{
MessageBox(NULL,_T("Drag & Drop operation be stopped!
\nOur NSE doesn't support drag&drop operation on folder object,
\nPlease make sure to drag file objects!"),
_T("NSExtDragDrop"),MB_OK);
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return E_FAIL;
}
}
// Handle the dragged files.
for( i=0; i<nFiles; i++ )
{
pChr = _tcsrchr(szFileName,_T('\\'));
_tcscpy(szItemName,pChr+1);
AddItemToCfgFile(pidlComplexDest,szItemName,NWS_FILE);
RefreshShellViewWndsExcept( NULL);
}
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return Hr;
}
HRESULT CMyVirtualFolder::_DoDrop_NSEDRAGDROP
(HGLOBAL hMem, DWORD dwDropEffect,LPITEMIDLIST pidlDropDest)
{
HRESULT Hr;
TCHAR szDropDest[MAX_PATH]=_T("");
DWORD dwSize=MAX_PATH;
LPITEMIDLIST pidlComplexDest = m_PidlMgr.Concatenate
(m_pidlPath,pidlDropDest);
if(pidlComplexDest) //Dest is not the root folder
{
HR(m_PidlMgr.GetFullName(pidlComplexDest,szDropDest,&dwSize));
if(dwSize == 0)
{
m_PidlMgr.Delete(pidlComplexDest);
return E_FAIL;
}
}
//Fetch Complex PIDL of dragged item from data object
LPITEMIDLIST pidlDragObject = (LPITEMIDLIST) ::GlobalLock(hMem);
if( pidlDragObject == NULL )
{
m_PidlMgr.Delete(pidlComplexDest);
return E_FAIL;
}
TCHAR szDragObject[MAX_PATH]=_T("");
dwSize=MAX_PATH;
HR(m_PidlMgr.GetFullName(pidlDragObject,szDragObject,&dwSize));
// if drag and drop in same path, do nothing
if( _DragDropInSamePath(szDragObject,szDropDest))
{
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return E_FAIL;
}
_tcsnset(szDragObject,0,MAX_PATH);
LPITEMIDLIST pidlTemp = m_PidlMgr.GetLastItem(pidlDragObject);
HR(m_PidlMgr.GetName(pidlTemp,szDragObject));
ITEM_TYPE iItemType = m_PidlMgr.GetItemType(pidlDragObject);
if(AddItemToCfgFile(pidlComplexDest,szDragObject,iItemType)==FALSE)
{
MessageBox(NULL,_T
("Destination Folder had include a same name file."),
_T("Name conflict"),MB_OK);
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return E_FAIL;
}
m_PidlMgr.Delete(pidlComplexDest);
::GlobalUnlock(hMem);
return Hr;
}
IShellView
Enable Shell View to be the Drop Target
To make Shell View be a drop target, all we need do is call RegisterDragDrop
function to register the listview Window as the target of an OLE drag-and-drop operation and specify the IDropTarget
instance to use for drop operations.
The IDropTarget
instance can be retrieved through call IShellFolder::CreateViewObject
to request an object which implemented IDropTarget
.
First, we should declare a IDropTarget
interface pointer as a data member of view object.
LPDROPTARGET m_pDropTarget; //provide the drop target service
HWND m_hwndList; //listview handler
And then create the drop target object and register it with listview after the view Window is created in IShellView::CreateViewWindow.
STDMETHODIMP CNSFShellView::CreateViewWindow(
LPSHELLVIEW lpPrevView,
LPCFOLDERSETTINGS lpFS,
LPSHELLBROWSER pSB,
LPRECT prcView,
HWND* phWnd)
{
...... //create host window for Shell View,
//which will send a WM_CREATE message
//and in handling WM_CREATE message, listview will be created
//to be the drop target
HR( m_pFolder->CreateViewObject(m_hWnd, IID_IDropTarget,
(LPVOID*) &m_pDropTarget) );
ATLASSERT(m_pDropTarget);
HR( ::RegisterDragDrop(m_hwndList, m_pDropTarget) );
return S_OK;
}
Of course, we should unregister it before the view Window will be destroyed.
STDMETHODIMP CNSFShellView::DestroyViewWindow(void)
{
//Unregister the drag drop
::RevokeDragDrop(m_hwndList);
m_pDropTarget->Release();
// Make absolutely sure all our UI is cleaned up.
UIActivate(SVUIA_DEACTIVATE);
::DestroyWindow(m_hWnd);
// Release the shell browser object
m_pShellBrowser->Release();
return S_OK;
}
Drag from Shell View
Shell View not only can be the drop target but can also act as a data source for an OLE drag-and-drop operation. The solution is to call DoDragDrop
when you detect that the user has started a drag-and-drop operation, that is, when handling LVN_BEGINDRAG
notification.
The DoDragDrop
function enters a loop in which it calls various methods in the IDropSource
and IDropTarget
interfaces.
Before calling DoDragDrop
function, you should prepare two objects: the data object that contains the data being dragged and drop source object which will be used to communicate with the source during the drag operation.
Which can described in following steps:
- Find the item to be dragged
- Prepare the source object and the data object according to the dragged item
- Call the
DoDragDrop
- Delete the dragged file from current listview.
LRESULT CNSFShellView::OnBeginDrag(UINT ClitID, LPNMHDR lpnmh, BOOL& bHandled)
{
// Find out the dragged item in listview
LPITEMIDLIST pidlSelected=NULL;
UINT nCount = ListView_GetSelectedCount(m_hwndList);
if( 0 == nCount)
{
pidlSelected = NULL;
return E_FAIL;
}
else
{
LV_ITEM lvItem;
int nSelItem = ListView_GetNextItem( m_hwndList, -1, LVIS_SELECTED );
ZeroMemory(&lvItem, sizeof(lvItem));
lvItem.mask = LVIF_PARAM;
lvItem.iItem = nSelItem;
if(ListView_GetItem(m_hwndList, &lvItem))
{
pidlSelected=(LPITEMIDLIST)(lvItem.lParam);
//Drag a folder from right panel is not allowed
if(NWS_FOLDER == m_PidlMgr.GetItemType(pidlSelected))
return E_FAIL;
}
}
// Create a IDataObject according to the selected item
CComObject<CNSFDATAOBJECT>* pDataObject;
HR( CComObject<CNSFDATAOBJECT>::CreateInstance(&pDataObject) );
pDataObject->AddRef();
//Called by our ShellView, pidl should be a complex pidl
LPITEMIDLIST pidlComplex;
pidlComplex = m_PidlMgr.Concatenate(m_pFolder->m_pidlPath,pidlSelected);
HR( pDataObject->_Init(m_pFolder, pidlComplex) );
if( SUCCEEDED(Hr) && (pDataObject!=NULL) )
{
// If we're dragging then call Window's DoDragDrop()
// method with a freshly created IDropSource object
CComObject<CNSFDROPSOURCE>* pDropSource;
HR( CComObject<CNSFDROPSOURCE>::CreateInstance(&pDropSource) );
pDropSource->AddRef();
// DoDragDrop, Only allows DROPEFFECT_MOVE....
DWORD dwEffect = 0;
Hr = ::DoDragDrop(pDataObject,pDropSource,DROPEFFECT_MOVE,&dwEffect);
pDropSource->Release();
if(Hr)
{
DeleteItemInCfgFile(pidlComplex);
Refresh();
RefreshShellViewWndsExcept(m_hWnd);
}
}
pDataObject->Release();
m_PidlMgr.Delete(pidlComplex);
return S_OK;
}
Conclusion
Hoo, all my articles about NSE finally finished. Writing articles is more challenging for me than writing code. However, you will find it will really help you to have a clearer insight into the topic and sometimes even make you aware of some subtle design deficiency.
I always see complaints about how boring NSE is :-). Here I share what I learned during develop my own NSE, and hope these will be of help to you.