Shell data objects, clipboard and drag-drop

I am confident that everybody has got enough of copying text in the clipboard this and the other way. It's high time we put all that knowledge in the service of our main goal, shell data transfer — moving files about. As we'll discover shortly, the only novelty will be a set of registered clipboard formats used by the shell; all the remaining principles we've learned in the last chapter will be applicable here, too. We're also going to see how data objects make clipboard handling and drag-drop nearly identical.

Topics: Shell Data Objects | Clipboard Transfers | Drag-Drop Operations

Shell Data Objects

Shell data transfers are all about moving "files" about, both real filesystem files or some item lying into a virtual folder like control panel. There is a number of clipboard formats for placing file information in data objects. Note that I am talking about file information, not the file contents themselves. It is more efficient to place the full path to a file in the clipboard, and have the "pasting" application read the data directly from the disc location.

If you press <Ctrl+C> to copy a couple of shortcuts lying on your desktop, and use the trusty Dataobject Viewer tool, here's the list of formats you'd see in the IDataObject created by explorer, who is running your desktop. There are five formats most of the time, irrespective of the actual number of files that were copied. Here's what each one of them means:

Out of all the clipboard formats used for shell data transfer, only CF_HDROP is defined as a system-wide constant. All the remaining formats are defined through symbolic names held in the various CFSTR_xxx constants. RegisterClipboardFormat can be used to convert the strings to more useful clipboard format identifiers, as usual.

Most formats are furnished through global memory handles (TYMED_HGLOBAL), since the data placed in the IDataObject are rather small in size. As a result, in order to read a format from a shell data object, you prepare a FORMATETC whose members have default values, i.e. NULL for ptd, DVASPECT_CONTENT for dwAspect, and -1 for lindex.

The most striking exception to this rule of simplicity is the CFSTR_FILEDESCRIPTOR format. This is mainly used within virtual folders that contain regular files albeit in some convoluted fashion, as for example archive (ZIP etc.) files, FTP servers, etc. A data object representing a bunch of files within such a folder will have the aforementioned CFSTR_FILEDESCRIPTOR, whose global handle will point to a FILEGROUPDESCRIPTOR structure. This is accompanied by the "individual" files, each in its own CFSTR_FILECONTENTS section. Since there may be many of these formats in a data object, the lindex member of FORMATETC must be set to specify exactly which file we're after. Finally, the most usual medium to extract such files is TYMED_ISTREAM instead of a global handle, in the usual case where the extraction is expensive in terms of CPU time.

We have already discussed the most important formats, from a file management point of view. There are a few more described in the official shell manuals but I never found any use for them myself, so I wouldn't be the right ambassador for them. It still beats me for example who on earth would ever care about a CFSTR_MOUNTEDVOLUME format identifier <g>.

Among the chaff, there still exist a few auxiliary formats which we will be looking at shortly. They don't describe files or folders directly but they still serve a significant role, allowing the source and the target to communicate. We have already seen how CFSTR_PREFERREDDROPEFFECT is used to clarify which operation is meant to be executed by the target. Later on we'll see how the target adds such auxiliary formats to the data object post-facto, informing the source of the operation that actually took place. Yes, you've read that right, clients of shell data objects are able — indeed expected — to write on objects created by servers that may even be in a different process altogether. Ain't data objects swell or what? <g>

ADVANCED: Data change notification
Shell data objects are in general short-lived, hence they don't support advice sinks for notifying interested parties of any changes that may occur to their contents through DAdvise. This however may leave some servers wondering what has happened to the data. For example, if an application places a few files in the clipboard meant to be cut, it will need to know the outcome of the operation, just in case it needs to delete the original files. Unfortunately there's no standard way for data objects placed on the clipboard to find out exactly what happened when. A possible solution would be to use some custom interface to communicate changes to the source application whenever SetData is called for CFSTR_PERFORMEDDROPEFFECT. Or alternatively you can do what most lazy people do, and assume that targets will perform optimized moves at all times shifting the burden on to them. Note that this situation is not an issue for drag-drop, since the source will know exactly when the drop took place, and can examine the final data object to discover what operation the client went for.

Clipboard Transfers

Knowing about shell clipboard formats is a good thing, but in practice you'd seldom want to build your very own data object to hold file information. We've seen in the last chapter how even a do-nearly-nothing implementation of IDataObject is far from trivial. A fully operational shell data object would have to provide functionality for external addition of arbitrary formats via SetData, which is a hard thing to manage. To cap it all, there are many different formats to deal with, each with its own peculiarities and unfamiliar data structures.

With a little bit of cunning though, we can let the shell dish out the goods for us. We have already seen the solution, complete with sample code: the shell context menu. The InvokeCommand command can be used to apply standard verbs to a bunch of files, that can both place data in the clipboard or even retrieve somebody else's clipboard shell data, with minimal effort — since the data object is created and managed by the shell. Here's a list of useful language-independent verbs:

VerbFunction
cutPlace shell item data in the clipboard, marking them for (requesting) DROPEFFECT_MOVE.
copyDitto for simple copying of files, without affecting the source.
pasteIf the clipboard contains file data, they can be pasted (i.e. copied or moved, depending on the requested drop-effect) on any valid drop target as for example any folder.
pastelinkSimilar to paste, this creates shortcuts to the files and places then within the target folder.

The whole functionality of an "Edit" menu can be thus obtained by simply putting together some code that utilizes the GetSHContextMenu and ProcessCMCommand functions presented earlier — extending them to handle more than one file is left as an exercise for the reader <g>. Obviously we wouldn't be using IContextMenu to show a shell menu here. We will just be using the file selection to determine which objects the action will affect and then pass the required verb to the InvokeCommand method, which will make it all happen.

When the cut or copy commands are used, the items whose IContextMenu pointer is obtained are themselves placed in the clipboard. For paste operations on the other hand, the target's shell context menu pointer is required, which will eventually receive files cut/copied earlier. When pasting an object that was cut, you should remove the data object from the clipboard so that it won't be pasted again. Fortunately invoking the "paste" verb will do this automatically for you, so no worries there. If you are wondering how the context menu figures out whether an item was cut or copied, it simply examines the CFSTR_PREFERREDDROPEFFECT auxiliary format; if this turns out to be DROPEFFECT_MOVE then it calls EmptyClipboard, else it leaves the data object intact, to be reused by other clients.

In the interest of completeness, since we are discussing the implementation of an Edit-like menu and all, ideally we would like to know whether these cut/copy/paste verbs are allowable so as to enable or disable the respective menu items. For example, most virtual folders won't permit pasting files in them, so you wouldn't want to mislead your users. GetAttributesOf comes to the rescue here, digging out the capability flags of each item in a number of SFGAO_CANxxx constants. SFGAO_DROPTARGET should be set if an item accepts paste operations; note that objects other than regular folders may be drop/paste targets, too, as for instance ZIP files. An additional requirement for enabling the "paste" menu item is for the clipboard to contain shell "paste-able" data. The easiest test is to call IsClipboardFormatAvailable for the CFSTR_SHELLIDLIST format. If you want to ensure that filesystem data exist then test for the CF_HDROP format instead.

IContextMenu can really make your life easy. However, if you are one of those types that you have to do everything your own way, well, brace yourself. You can take a look at the "FileDrag.exe" sample referenced in the additional information in the bottom of this page, to get an idea how to manage all those different shell clipboard formats. I have to warn you, that I've been through the code in that sample, and without being overly inquisitive I have discovered a number of potential leaks, especially in their SetData implementation.

Pasting shell data from the clipboard on the other hand, is not very hard, so you may be tempted to forgo the "paste" verb approach described above and go solo instead. Here's a code snippet that examines the clipboard for CF_HDROP and if present enumerates the pathnames. If you are familiar with WM_DROPFILES windows message, you'll recognise the similarity.
#include <shellapi.h>

LPDATAOBJECT pDO;
// open the clipboard and ask for a CF_HDROP, if any
HRESULT hr = OleGetClipboard(&pDO);

FORMATETC fmt = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgm;
hr = pDO->GetData(&fmt, &stgm);
if(S_OK == hr) {
   UINT nFiles = DragQueryFile((HDROP)stgm.hGlobal, -1, NULL, 0);

   TCHAR szPath[MAX_PATH];
   for(int i = 0; i < nFiles; i++) {
      DragQueryFile((HDROP)stgm.hGlobal, i, szPath, MAX_PATH);
      // do something useful with the path...
   }
   ReleaseStgMedium(&stgm); // will call GlobalFree
}
// else there are no file data in the clipboard

pDO->Release(); // "closes" the clipboard

After opening the clipboard, we query the data object for CF_HDROP served in global memory. If that's successful, then DragQueryFile is called repeatedly to get the full path names. First we set iFile to -1 which instructs DragQueryFile to tell us the total number of files in the clipboard. Then we get each name in turn. After all is said and done, we take care of cleaning up the global memory via ReleaseStgMedium and naturally release the data object itself.

A gray point is whether DragFinish should be called, too. The docs aren't clear on this, and I've seen samples both calling it and refrain from doing so — but we should know better not to trust samples! <g> Anyway, in the end of the day I have opted not to call the darn thing, which seems to be only appropriate within WM_DROPFILES handlers, where the clipboard or dropped object is accessed behind our backs. Still I am hereby disclaiming all responsibility for any memory leaks that you may suffer as a result of this recommendation of mine. The true culprit is tiny$oft, and its poor documentation <g>.

ADVANCED: Windows 9x and that derranged clipboard
Windows 9x default implementation of IContextMenu for shell folders is flawed, since it will forget including a CFSTR_PREFERREDDROPEFFECT auxiliary format in the clipboard when InvokeCommand is used for either cut or copy verbs. This is a serious problem, leaving potential paste commands at a loss regarding the fate of the source file(s): should they be deleted after paste or what? The workaround is to make amends for the shortcomings of win9x and add the missing format manually — or is it? It turns out that once a data object is placed in the clipboard, it is no longer possible to inject new formats with SetData, even if in its original nick it would have allowed such an addition. I don't really know why this is so, I can just confirm the behaviour. Perhaps those proxy data objects are to blame. The most infuriating aspect is that SetData would seemingly succeed and return S_OK, whereas no data are really added. I have even tried the following with no success:
  • Created my own data object which accepts additional formats on the fly.
  • Created an instance of it and placed it in the clipboard.
  • Used its direct IDataObject pointer — which I knew since it was I that instantiated it in the first place — to successfully add a new format.
  • Opened the clipboard and enumerated the formats of my own object as seen by the proxy data object.
  • The added format was nowhere to be found; sure enough, enumerating the direct IDataObject would properly show the same extra format.
Obviously, there seems to be a lack of communication between the real and the clipboard proxy data objects. The solution? Please read on for an alternative way of obtaining shell data objects for free...

Drag-Drop Operations

Drag-drop is the holy grail of the beginner windows programmer. Once mastered, it immediately separates one from the crowd of "boyz"; it's an entitlement of entry to the league of seasoned programmers. Drag-drop data transfer is invariably much harder than the clipboard. There are no API conveniences here, it's COM all the way. Still, IDataOject remains the key object, which should be already familiar by now. There are a couple new interfaces particular to drag-drop transfers, but as we're going to see, they are much simpler to work with.

Drag-drop combines a copy and a paste operation in a single convenient mouse-controlled action, which transfers data from one application to another — or even within a single application. Any window can be a drag source. It packs the selected items into a data object, in an appropriate format (e.g. CF_TEXT if characters are transferred etc.). Then, instead of placing the data in the clipboard it calls DoDragDrop to kick off the operation.

The data transfer OLE subsystem then takes control, examining windows underneath the mouse cursor in search of a potential drop target. A window is not automatically eligible to accept dragged objects. RegisterDragDrop must be used to subscribe to the list of drop targets. After this initiation ceremony, whenever the mouse enters our window we are given a pointer to the IDataObject being transferred, which we can examine for any clipboard formats we could possibly accept. If we can understand the data contents and the user releases the left mouse button, data are effectively pasted on our window. When we are no longer interested in accepting dropped data (e.g. when the window is destroyed) RevokeDragDrop should be used to take us off the spam list <g>.

The drag source application is blocked until DoDragDrop returns, i.e. either a drop has taken place pasting the data or the operation was cancelled. In order to complete the operation, the source must examine the pdwEffect argument returned by DoDragDrop. If it is equal to DROPEFFECT_MOVE the source must delete its own copy of the data just transferred, simulating a "cut" operation. Finally the data object itself is released and the drill is all over.

That is the outline, let us now concentrate on the implementation details. Applications are usually both drag sources and drop targets, for the data formats they work with, which in our case is shell items. Implementing the drag source portion is easier, so let us look into this first.

We need to pack the files/folders to be transferred in an IDataObject, similar to the one examined in the previous clipboard section. Once again we are laming about <g> and would prefer if someone else got into the trouble of building and maintaining the data object for us, thank you very much. We cannot use the same trick here with IContextMenu though, since that would overwrite the clipboard contents — drag-drop is meant to bypass the clipboard completely.

The solution is to use GetUIObjectOf and request an IDataObject pointer for a bunch of selected shell items. This is similar to the context menu querying procedure, only here we pass IID_IDataObject in the riid argument. No sweat whatsoever. Not only do we get all the data packed for free in all relevant formats (CF_HDROP, CFSTR_SHELLIDLIST et al.), we also inherit a dynamic IDataObject implementation which can accept arbitrary additional formats on the fly — I even managed to add CF_TEXT on it using SetData (!).

Obviously the same technique can be used to obtain a data object to be placed in the clipboard, avoiding the context menu approach. The real boon is that we can circumvent the problem of the missing CFSTR_PREFERREDDROPEFFECT format in win9x mentioned earlier. Using a "raw" IDataObject we can add this or any other auxiliary format as easy as:
HRESULT AddValue2DataObject(LPDATAOBJECT pDO, UINT cFormat, DWORD value)
{
   // test if the format is already there
   FORMATETC fmt = {cFormat, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
   HRESULT hr = pDO->QueryGetData(&fmt);
   if(hr == S_OK) return S_FALSE; // already there, don't add again

   // copy the supplied value to global memory and insert it in the data object
   HGLOBAL hMem = GlobalAlloc(GMEM_SHARE | GMEM_MOVEABLE, sizeof(DWORD));
   DWORD* pdwVal =(DWORD*)GlobalLock(hMem);
   *pdwVal = value;
   GlobalUnlock(hMem);
   STGMEDIUM stgm = {TYMED_HGLOBAL, NULL, NULL};
   stgm.hGlobal = hMem; // compilers ain't too smart theze dayz :)
   return pDO->SetData(&fmt, &stgm, TRUE/*HIS responsibility*/);
}

This is a generic function that you can use to add any DWORD in a data object that supports auxiliary formats. First we check if the format is already there calling QueryGetData. If not, we allocate a single DWORD from global memory, fill in the passed value and finally add the format to the data object. Note that by setting fRelease parameter to TRUE, we instruct SetData to take responsibility for the allocated global memory, and Bob is our uncle <g>.

With the data object sorted, only a small obstacle remains before calling DoDragDrop. It expects a pointer to an IDropSource interface, which is a very simple object which mainly monitors the mouse and keyboard to determine e.g. when the user drops the data, and provides mouse cursors to reflect the keyboard state — which in turn determines the drop action. It is a less known feature of drag-drop that keyboard modifiers can be used to force a particular transfer operation. Here's a list of the standard modifiers and their action:

Key combinationDrop effect
No keys or <Shift>DROPEFFECT_MOVE: data are moved from the original container (folder) to the drop target.
<Ctrl>DROPEFFECT_COPY: object is copied from source to target — two instances of the same object will exist.
<Ctrl+Shift>DROPEFFECT_LINK: target receives a link (shortcut) to the source data — the latter remain unmodified.

IDropSource is the interface that makes sure these default actions are taken, and provides visual cues modifying the mouse cursor — but this should not be confused with images being dragged. Anyway, once you have a basic IDropSource implementation you can use it all over the place, regardless of the actual data content being dragged. For minimal effort you may use MFC's COleDropSource which is cheap and cheerful. Let's summarize the drag initiation action:
// (within a suitable handler e.g. LVN_BEGINDRAG...)
LPDATAOBJECT pDO; // filled using GetUIObjectOf on the selection
COleDropSource dropSrc;

// a little hocus-pocus to use COleDropSource stand-alone...
dropSrc.m_dwButtonDrop = MK_LBUTTON; // assume left dragging
dropSrc.m_dwButtonCancel = MK_RBUTTON;
dropSrc.m_bDragStarted = TRUE;

// extract pure interface pointer off the COle object
LPDROPSOURCE pDropSrc = 
   (LPDROPSOURCE)dropSrc.GetInterface(&IID_IDropSource);
// enable all possible drop effects for targets
DWORD dwOKEffect = DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK;
DWORD dwResultEffect = DROPEFFECT_NONE;
if ( DRAGDROP_S_DROP == 
     DoDragDrop(pDO, pDropSrc, dwOKEffect, &dwResultEffect) ) 
{
   // examine dwResultEffect
   // display may need refreshing
}

pDO->Release();
// pDropSrc needn't be released, apparently

The most complicated aspect of this pseudo-code snippet is the initialisation of the COleDropSource object, which isn't really meant to be used on its own, without the companion COleDataSource. Still, a little poking around in the MFC source files reveals the necessary adjustments. Loads of undocumented stuff here, including that GetInterface method inherited from CCmdTarget (found in Oleunk.cpp). This returns the actual COM interface pointer implemented by COleDropSource, which for some reason doesn't need to be released after use. Which is all rather confusing and un-COM-like. That's the reason why I have personally abandoned using all those COlexxx convenience objects, which in the end of the day just give you sleepless nights complete with memory leak nightmares <g>. Anyway, the certain thing is that the IDataObject we obtained from the shell folder object must be released as usual.

Dropping on targets

The implementation of drop targets is a wee bit more contrived. RegisterDragDrop expects a pointer to a IDropTarget interface, which the target needs to implement. When data are dragged over a registered target window, the OLE subsystem calls various methods of IDropTarget passing information on current affairs and also quering the target whether it can accept the data. In the end, if the user drops the data, the Drop method is called, and the actual pasting can take place, which can be in any DROPEFFECT_xxx "mode".

Well, I've got good news and bad news. The good news is that we can use GetUIObjectOf again — this method totaly saves our skin <g> — to obtain an IDropTarget interface for any shell item that can be a valid drop target (e.g. a folder, a ZIP file, etc.). The bad news is that we need another drop target interface to pass to RegisterDragDrop that would be associated with our view window, transcending any shell items that may be contained/displayed. But even this obstacle may be side-stepped using MFC's COleDropTarget class which implements IDropTarget for our window.

I think that the best way to explain the process is via a complete project. Just unpack the archive and build the MFC project — I have just tried it with version 6.0 of the developer studio. Once up and running, you will be confronted with a dialog window which can accept data dragged from various sources, dropping (pasting) them in the nominated target folder — which can also be a ZIP file or whatever. You'll have to experiment to see what's going on, using various keyboard modifiers and mouse buttons. Would it surprise you to learn that our humble target would even accept text dragged from M/S Word? — just check it out!

The source code is well commented, so here I'll just give you a quick tour, highlighting the most important points. It is a standard dialog-based MFC application. There's a text box for accepting target full paths — make sure path is valid else nowt will work. The most interesting item is the CStatic window with the target-like bitmap, all on account of its drop target capabilities. You'll notice that during the dialog initialization RegisterDragDrop is used to register it with the OLE subsystem. There's only one exit from the dialog, where RevokeDragDrop is used completing the circle.

The most interesting and relevant class is CDropTarget, which is totally MFC-free — to the rejoice of many of you anti-MFC folk out there <g>. This implements the IDropTarget interface in a pseudo-COM object which is meant to be used on the stack or as a class member variable (if you remember the CDataObjectImpl sample class you'll recognise the similarity). If the class looks a bit complicated for you, you may ignore all the ASSERT() statements — that's just a sign of my insecure persona <g> — and the debug-only sections held within "#ifdef _DEBUG / #endif" precompiler directives, which are there only for richer information and yet more reassurance.

This class furnishes the IDropTarget associated with the static target window via RegisterDragDrop. Despite the apparent complexity, the class does very little itself, at least from a drop target point of view. The first method called (by drag-drop OLE) is DragEnter, when any unidentified dragged item enters the window's aerial space. This is the heart of our implementation. Through a series of roundabout steps, the path name specified in the dialog box is converted to an IDropTarget interface, representing the shell object in question. We've seen the drill before, and in the end of the day it is GetUIObjectOf that does all the magic, as professed.

From that point onwards, our responsibility is over. We just relay our IDropTarget calls to the shell interface. We only act as a medium, a broker so to speak. We don't need to know anything about shell clipboard formats, we don't need to monitor the keyboard during the repeated invocations of DragOver, we don't even care whether the target is a folder or something exotic — we just sit back and relax, waiting for an outcome. There are two possible routes:

  • No drop occurs. Either the mouse cursor has left our window or the drag was cancelled e.g. by pressing <Esc>. Our DragLeave method is called allowing us to cleanup.
  • User dropped the data. When the mouse button that initiated the drag is released over our window, our Drop is called, whereupon the data are "pasted" on the target. Of course we couldn't care less about how the drop takes place, we just delegate to the shell interface as usual, which will implement the requested pdwEffect by copying/moving or even creating shortcuts to the data. Note that DragLeave is not called afterwards, so we have to duplicate the cleanup actions in Drop as necessary.

Easy as pie! After the drop operation is finished, both source and target applications are released by the clutches of DoDragDrop and they can resume business as usual. You may want to read about some transfer scenario handling that is meant to be done, but thankfully you don't have to worry about anything since the shell item's IDropTarget will update the dragged object as necessary, filling in the performed drop effect for the information of the drop source.

All this "transfer scenario" hype basically boils down to optimized moves. What this means is when data are dropped with DROPEFFECT_MOVE, the target uses SHFileOperation to move the data in a single stroke, rather than copying the files first and then notifying the source to delete the source files — which is all common sense really. As I said, your app needn't be concerned with any of this, since the real work of both drag source and drop target is performed by the shell itself, if you follow my recommended route. All you need to deal with is updating your window display (if any) to show any new files added or removed during drag-drop.

ADVANCED: Proxy drag objects?
You may remember the discussion in the clipboard section earlier about the proxy data objects used by the clipboard, which wrap the real IDataObject you set, and make a really bad job of it. Well, it seems that the same holds for drag-drop. If you examine the output of DumpDataObject() function (in the sample project), you'll see that "apparently" after the Drop has taken place, the dragged data object has exactly the same formats. This is not true, since if you directly query the object for CFSTR_PERFORMEDDROPEFFECT via QueryGetData, you'll discover that this additional format exists, and the value is actually equal to the pdwEffect set by Drop. So what's the story? It looks like the proxy object is actually accepting new formats on the fly, but its IEnumFORMATETC is flawed since it takes an initial content snapshot during the beginning of the drag and remains oblivious to any new formats added via SetData. Which is all a rather poor show by little$oft... <g> Let me reiterate that all this discussion is strictly academic.

Additional information

Q185572 - HOWTO: Get Dropped File Names Using OLE Drag and Drop
Q83543 - DragDrop.exe - Implementing the Drag-Drop Protocol
Q139067 - SAMPLE: FileDrag.exe Supports File Drag Server Capabilities
ARTICLE - The Shell Drag/Drop Helper Object
Q182219 - PRB: Shell Returns DROPEFFECT_NONE Instead of DROPEFFECT_MOVE



Clipboard Autocompletion Contents