If you've never seen OLE drag-and-drop in action, you can perform a simple demonstration using the source code editor in Visual C++. Begin by opening a source code file and highlighting a line of text. Grab the highlighted text with the left mouse button, and with the button held down, drag it down a few lines. Then release the mouse button. The text will disappear from its original location and appear where you dropped it, just as if you had performed a cut-and-paste operation. Repeat the operation with the Ctrl key held down, and the text will be copied rather than moved. That's OLE drag-and-drop. You used it to transfer text from one part of a document to another, but it works just as well if the destination is a different document or even a different application. And just as with the OLE clipboard, you can use OLE drag-and-drop to transfer any kind of data—not just text.
Programmatically, OLE drag-and-drop is very similar to the OLE clipboard. The data provider, or drop source, creates a data object that encapsulates the data and makes an IDataObject pointer available. The data consumer, or drop target, retrieves the IDataObject pointer and uses it to extract data from the data object.
One difference between OLE drag-and-drop and the OLE clipboard is how the IDataObject pointer changes hands. The OLE clipboard uses ::OleSetClipboard and ::OleGetClipboard to transfer the pointer from sender to receiver. In OLE drag-and-drop, the drop source initiates a drag-and-drop operation by passing an IDataObject pointer to ::DoDragDrop. On the other end, any window interested in being a drop target registers itself with the system by calling the API function ::RegisterDragDrop. If a drop occurs over a window that's registered in this way, the drop target is handed the IDataObject pointer passed to ::DoDragDrop.
If that's all there was to it, OLE drag-and-drop wouldn't be difficult at all. What complicates matters is that OLE drag-and-drop requires three COM objects instead of just one:
The data object is identical to the one used for OLE clipboard transfers. The drop source and drop target objects are new. Figure 19-2 shows a schematic representation of the participants in a drag-and-drop data transfer. On the sending end of the transaction is an application that implements two COM objects: a data object and a drop source object. (There's nothing to prevent one object from supporting both interfaces, but in practice, the objects are usually implemented separately.) On the receiving end is an application that implements a drop target object. Neither the drop source nor the drop target receives an IDropSource or IDropTarget pointer that references the other. Instead, the system acts as an intermediary and calls methods on both interfaces at the appropriate times.
Figure 19-2. Participants in an OLE drag-and-drop operation.
An OLE drag-and-drop operation begins when an application calls ::DoDragDrop and passes in four key pieces of information:
::DoDragDrop returns when either of two conditions is met:
The action that cancels a drag-and-drop operation varies from application to application and is ultimately determined by the drop source. In most cases, the stimulus is a press of the Esc key. If the operation is canceled or the drop target rejects the drop, ::DoDragDrop copies the value DROPEFFECT_NONE to the address in the fourth parameter. If the drop is successful, ::DoDragDrop copies one of the DROPEFFECT codes passed in the third parameter to the address in the fourth parameter so that the drop source will know precisely what occurred.
Assume that pdo and pds hold IDataObject and IDropSource pointers, respectively. The following statements initiate a drag-and-drop operation in which the data encapsulated in the data object can be either moved or copied:
DWORD dwEffect; HRESULT hr = ::DoDragDrop (pdo, pds, DROPEFFECT_MOVE | DROPEFFECT_COPY, &dwEffect); |
When ::DoDragDrop returns, dwEffect tells the drop source what transpired on the other end. If dwEffect equals DROPEFFECT_NONE or DROPEFFECT_COPY, the drop source doesn't need to do anything more. If dwEffect equals DROPEFFECT_MOVE, however, the drop source must delete the data from the source document:
if (SUCCEEDED (hr) && dwEffect == DROPEFFECT_MOVE) { // Delete the original data from the document. } |
The code that deletes the data isn't shown because, obviously, it's application-specific.
Calls to ::DoDragDrop are synchronous; that is, ::DoDragDrop doesn't return until the operation has been completed or canceled. However, as a drag-and-drop operation is being performed, the system communicates with the drop source through the IDropSource pointer provided to ::DoDragDrop. IDropSource is a simple interface that contains just two methods besides the IUnknown methods common to all COM interfaces:
IDropSource Methods
Method | Description |
---|---|
GiveFeedback | Called each time the cursor moves or a key state changes to allow the drop source to update the cursor |
QueryContinueDrag | Called when a key state or mouse button state changes to allow the drop source to specify whether to continue the operation, cancel it, or execute a drop |
Whenever a change occurs in the state of a key or mouse button that might be of interest to the drop source, the drop source object's QueryContinueDrag method is called. QueryContinueDrag receives two parameters: a BOOL indicating whether the Esc key has been pressed and a DWORD containing flags that reflect the current state of the mouse buttons as well as the Ctrl, Alt, and Shift keys. Using this information, QueryContinueDrag must return one of three values telling the system what to do next:
Return Value | Description |
---|---|
S_OK | Continue the drag-and-drop operation |
DRAGDROP_S_DROP | End the operation by executing a drop |
DRAGDROP_S_CANCEL | Cancel the drag-and-drop operation |
Typical responses are to cancel the operation if the Esc key has been pressed, to execute a drop if the left mouse button has been released, or to allow the operation to continue if neither of the first two conditions is true. The following QueryContinueDrag implementation embodies this logic:
HRESULT __stdcall CDropSource::QueryContinueDrag (BOOL fEscape, DWORD grfKeyState) { if (fEscape) return DRAGDROP_S_CANCEL; // Esc key was pressed. if (!(grfKeyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // Left mouse button was released. return S_OK; // Let the operation continue. } |
This code assumes that the drag-and-drop operation began when the left mouse button was depressed. If you're implementing right-button drag instead, check the right mouse button (MK_RBUTTON) to determine whether to execute the drop. If you prefer to use a key other than Esc to cancel the operation, you can call ::GetAsyncKeyState to read the key's state and use that value rather than fEscape to decide whether to return DRAGDROP_S_CANCEL.
As a drag-and-drop operation unfolds, the drop source receives a flurry of calls to its IDropSource::GiveFeedback method. GiveFeedback receives one function parameter: a DROPEFFECT code that tells the drop source what would happen if a drop were to occur right now. (As you'll see in the next section, this information comes from the drop target because ultimately it's the drop target that controls what happens on the other end.) GiveFeedback's job is to inspect this parameter and update the cursor to provide visual feedback to the user. When you see the cursor change shape as it moves from window to window during a drag-and-drop data transfer or when you see a little plus sign appear next to the cursor when the Ctrl key is pressed, what you're actually seeing is the drop source's response to IDropSource::GiveFeedback.
If you want to, you can create your own cursors and display them each time GiveFeedback is called; however, the system provides several predefined cursors for just this purpose. To use them, simply return DRAGDROP_S_USEDEFAULTCURSORS from your GiveFeedback implementation. Rather than do this:
HRESULT __stdcall CDropSource::GiveFeedback (DWORD dwEffect) { HCURSOR hCursor; switch (dwEffect) { // Inspect dwEffect, and load a cursor handle in hCursor. } ::SetCursor (hCursor); return S_OK; } |
you can do this:
HRESULT __stdcall CDropSource::GiveFeedback (DWORD dwEffect) { return DRAGDROP_S_USEDEFAULTCURSORS; } |
That's all there is to most implementations of IDropSource::GiveFeedback. You can do more if you'd like, but you might as well use the default cursors unless you have compelling reasons to do otherwise.
A window becomes an OLE drop target when an application calls ::RegisterDragDrop and passes in the window's handle and a pointer to an IDropTarget interface:
::RegisterDragDrop (hWnd, pdt); |
You unregister a drop target by calling ::RevokeDragDrop. Although the system will clean up after you if you fail to call this function before a drop target window is destroyed, calling it yourself is good form.
When the cursor enters, leaves, or moves over a drop target window during a drag-and-drop operation, the system apprises the drop target of that fact by calling IDropTarget methods through the IDropTarget pointer provided to ::RegisterDragDrop. IDropTarget has just the four methods listed in the following table.
IDropTarget Methods
Method | Description |
---|---|
DragEnter | Called when the cursor enters the drop target window |
DragOver | Called as the cursor moves over the drop target window |
DragLeave | Called when the cursor leaves the drop target window or if the operation is canceled while the cursor is over the window |
Drop | Called when a drop occurs |
Both DragEnter and DragOver receive a pointer to a DWORD (among other things) in their parameter lists. When either of these methods is called, the drop target must let the drop source know what would happen if a drop were to occur by copying a DROPEFFECT value to the DWORD. The value copied to the DWORD is the value passed to the drop source's GiveFeedback method. DragEnter and DragOver also receive a set of cursor coordinates (in case the outcome of a drop depends on the current cursor position) and flags that specify the status of the Ctrl, Alt, and Shift keys and each of the mouse buttons. In addition, DragEnter receives an IDataObject pointer that it can use to query the data object. The following implementations of DragEnter and DragOver return DROPEFFECT_NONE, DROPEFFECT_MOVE, or DROPEFFECT_COPY to the data source depending on whether text is available from the data object and whether the Ctrl key is up (move) or down (copy):
HRESULT __stdcall CDropTarget::DragEnter (IDataObject* pDataObject, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect) { FORMATETC fe = { CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; if (pDataObject->QueryGetData (&fe) == S_OK) { m_bCanAcceptData = TRUE; *pdwEffect = (grfKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; } else { m_bCanAcceptData = FALSE; *pdwEffect = DROPEFFECT_NONE; } return S_OK; } HRESULT __stdcall CDropTarget::DragOver (DWORD grfKeyState, POINTL pt, DWORD* pdwEffect) { if (m_bCanAcceptData) *pdwEffect = (grfKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; else *pdwEffect = DROPEFFECT_NONE; return S_OK; } |
m_bCanAcceptData is a BOOL member variable that keeps a record of whether the data offered by the drop source is in a format that the drop target will accept. When DragOver is called, the drop target uses this value to determine whether to indicate that it's willing to accept a drop.
The drop target's DragLeave method is called if the cursor leaves the drop target window without executing a drop or if the drag-and-drop operation is canceled while the cursor is over the drop target window. The call to DragLeave gives the drop target the opportunity to clean up after itself by freeing any resources allocated in DragEnter or DragOver if the anticipated drop doesn't occur.
The final IDropTarget method, Drop, is called if (and only if) a drop occurs. Through its parameter list, Drop receives all the information it needs to process the drop, including an IDataObject pointer; a DWORD that specifies the state of the Ctrl, Alt, and Shift keys and the mouse buttons; and cursor coordinates. It also receives a DWORD pointer to which it must copy a DROPEFFECT value that informs the data source what happened as a result of the drop. The following Drop implementation retrieves a text string from the data object, provided that a text string is available:
HRESULT __stdcall CDropTarget::Drop (IDataObject* pDataObject, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect) { if (m_bCanAcceptData) { FORMATETC fe = { CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; STGMEDIUM stgm; if (SUCCEEDED (pDataObject->GetData (&fe, &stgm)) && stgm.hGlobal != NULL) { // Copy the string from the global memory block. . . . ::ReleaseStgMedium (&stgm); *pdwEffect = (grfKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; return S_OK; } } // If we make it to here, the drop did not succeed. *pdwEffect = DROPEFFECT_NONE; return S_OK; } |
A call to Drop isn't followed by a call to DragLeave, so if there's any cleaning up to do after the drop is completed, the Drop method should do it.
Most of the work in writing OLE drag-and-drop code lies in implementing the COM objects. Fortunately, MFC will implement them for you. The same COleDataSource class that provides data objects for OLE clipboard operations works with OLE drag-and-drop, too. COleDropSource provides a handy implementation of the drop source object, and COleDropTarget provides the drop target object. Very often, you don't even have to instantiate COleDropSource yourself because COleDataSource does it for you. You will have to instantiate COleDropTarget, but you usually do that simply by adding a COleDropTarget member variable to the application's view class.
Suppose you'd like to transfer a text string using OLE drag-and-drop in an MFC application. Here's how to do it using a global memory block as the storage medium:
char szText[] = "Hello, world"; HANDLE hData = ::GlobalAlloc (GMEM_MOVEABLE, ::lstrlen (szText) + 1) LPSTR pData = (LPSTR) ::GlobalLock (hData); ::lstrcpy (pData, szText); ::GlobalUnlock (hData); COleDataSource ods; ods.CacheGlobalData (CF_TEXT, hData); DROPEFFECT de = ods.DoDragDrop (DROPEFFECT_MOVE | DROPEFFECT_COPY) if (de == DROPEFFECT_MOVE) { // Delete the string from the document. } |
This code is strikingly similar to the code presented earlier in this chapter that used COleDataSource to place a text string on the OLE clipboard. Other than the fact that the COleDataSource object is created on the stack rather than on the heap (which is correct because, in this case, the object doesn't need to outlive the function that created it), the only real difference is that COleDataSource::DoDragDrop is called instead of COleDataSource::SetClipboard. COleDataSource::DoDragDrop is a wrapper around the API function of the same name. In addition to calling ::DoDragDrop for you, it also creates the COleDropSource object whose IDropSource interface pointer is passed to ::DoDragDrop.
If you'd rather create your own COleDropSource object, you can do so and pass it by address to COleDataSource::DoDragDrop in that function's optional third parameter. The only reason to create this object yourself is if you want to derive a class from COleDropSource and use it instead of COleDropSource. Programmers occasionally derive from COleDropSource and override its GiveFeedback and QueryContinueDrag member functions to provide custom responses to the IDropSource methods of the same names.
MFC makes acting as a target for OLE drag-and-drop data transfers relatively easy, too. The first thing you do is add a COleDropTarget data member to the application's view class:
// In CMyView's class declaration COleDropTarget m_oleDropTarget; |
Then, in the view's OnCreate function, you call COleDropTarget::Register and pass in a pointer to the view object:
m_oleDropTarget.Register (this); |
Finally, you override the view's OnDragEnter, OnDragOver, OnDragLeave, and OnDrop functions or some combination of them. These CView functions are coupled to the similarly named IDropTarget methods. For example, when the drop target object's IDropTarget::Drop method is called, COleDropTarget::OnDrop calls your view's OnDrop function. To respond to calls to IDropTarget::Drop, you simply override CView::OnDrop.
Here's an example that demonstrates how to override OnDragEnter, OnDragOver, and OnDrop in a CScrollView-derived class to make the view a drop target for text. OnDragLeave isn't overridden in this example because nothing special needs to be done when it's called. Notice that a preallocated COleDataObject is provided in each function's parameter list. This COleDataObject wraps the IDataObject pointer passed to the drop target's IDropTarget methods:
DROPEFFECT CMyView::OnDragEnter (COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) { CScrollView::OnDragEnter (pDataObject, dwKeyState, point); if (!pDataObject->IsDataAvailable (CF_TEXT)) return DROPEFFECT_NONE; return (dwKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; } DROPEFFECT CMyView::OnDragOver (COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) { CScrollView::OnDragOver (pDataObject, dwKeyState, point); if (!pDataObject->IsDataAvailable (CF_TEXT)) return DROPEFFECT_NONE; return (dwKeyState & MK_CONTROL) ? DROPEFFECT_COPY : DROPEFFECT_MOVE; } BOOL CMyView::OnDrop (COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) { CScrollView::OnDrop (pDataObject, dropEffect, point); HANDLE hData = pDataObject->GetGlobalData (CF_TEXT); if (hData != NULL) { // Copy the string from the global memory block. . . . ::GlobalFree (hData); return TRUE; // Drop succeeded. } return FALSE; // Drop failed. } |
This code looks a lot like the non-MFC version presented in the previous section. OnDragEnter and OnDragOver call COleDataObject::IsDataAvailable through the pointer provided in their parameter lists to determine whether text is available. If the answer is no, both functions return DROPEFFECT_NONE to indicate that they won't accept the drop. The drop source, in turn, will probably display a "no-drop" cursor. If text is available, OnDragEnter and OnDragOver return either DROPEFFECT_MOVE or DROPEFFECT_COPY, depending on whether the Ctrl key is down. OnDrop uses COleDataObject::GetGlobalData to retrieve the data when a drop occurs.
The examples in the previous section assume that the drop target is a view-based application. You can use COleDropTarget to implement drop targeting in applications that don't have views by deriving your own class from COleDropTarget and overriding OnDragEnter, OnDragOver, OnDragLeave, and OnDrop. However, using a view as a drop target offers one very attractive benefit if the drop target has scroll bars: you get drop target scrolling for free, courtesy of MFC.
What is drop target scrolling? Suppose a drag-and-drop operation has begun and the user wants to drop the data at a location in a CScrollView that is currently scrolled out of sight. If the cursor pauses within a few pixels of the view's border, a CScrollView will automatically scroll itself for as long as the cursor remains in that vicinity. Thus, the user can move the cursor to the edge of the window and wait until the drop point scrolls into view. This is just one more detail you'd have to handle yourself if MFC didn't do if for you.