OLE provides an elegant, simple, standardized way to implement drag and drop capability in applications.
To explore drag and drop support in this chapter, we build an OLE container application. This simple application is able to hold a series of container objects. Via the OLE drag and drop mechanism, this application supports dragging items between its view and another application, between its own view windows, and within a single view window.
Drag and drop represents a technique of sharing data between applications that act as drag sources and applications that act as drop targets. Drag sources are applications that enable their items to be dragged from their windows to the windows of other applications. Drop targets are applications that accept items dragged from drag sources and released within their window (Figure 31.1).
Implementing a drag source using MFC is very simple. Implementing a drop target is somewhat more difficult, but it is still not an overwhelmingly complex task.
The simple implementations presented in this chapter are based on the drag and drop support in the class COleClientItem. In applications that support drag and drop of native data or drag and drop of multiple items, you may decide to utilize the drag and drop support in COleServerItem instead.
Drag and drop functionality and clipboard functionality have many things in common. If you wish to implement clipboard cut, copy, and paste functions using OLE, you can share most of the clipboard support code and drag and drop code.
The container application required for our purposes is based on the AppWizard-generated container application default, with a few modifications that make it possible to select items using the mouse and that support persistent storage of item positions.
To create the application, use AppWizard's defaults, except for specifying container application support in AppWizard Step 3. I named the application OCON for the simple reason that it is based on the example I presented in Chapter 30. Let me now briefly review the changes that need to be made to the AppWizard-generated skeleton to support persistent storage of embedded object positions and to support item selection using the mouse.
The default AppWizard-supplied implementation of a container application positions inserted objects using a fixed rectangle. To make it possible for inserted items to be positioned anywhere in the drawing surface, a member variable of type CRect must be added to the class COCONCntrItem. This variable must also be serialized. The Draw member function of the view class must utilize this variable when drawing objects.
This variable should also be utilized in the COCONCntrItem member functions OnChangeItemPosition and OnGetItemPosition. Another change to COCONCntrItem is the addition of a new helper function, Invalidate; this function, taking a CWnd pointer as its parameter, invalidates the item's rectangle in the specified window.
To add the new member variable to the COCONCntrItem class and to declare the new helper function, Invalidate, use the following code:
class COCONCntrItem : public COleClientItem { ... // Attributes public: ... CRect m_rect; ... // Operations public: void Invalidate(CWnd *pWnd); // Implementation ...
This member variable must be initialized in the COCONCntrItem constructor (Listing 31.1).
COCONCntrItem::COCONCntrItem(COCONDoc* pContainer) : COleClientItem(pContainer) { // TODO: add one-time construction code here m_rect = CRect(0, 0, 0, 0); }
The modified versions of COCONCntrItem::OnChangeItemPosition and COCONCntrItem::OnGetItemPosition (Listing 31.2) are used to reflect any changes in the item's size or position that were made during an in-place session. It also makes use of the GetCachedExtent member function to update the item's size and position if it has not yet been initialized.
BOOL COCONCntrItem::OnChangeItemPosition(const CRect& rectPos) { ASSERT_VALID(this); if (!COleClientItem::OnChangeItemPosition(rectPos)) return FALSE; // TODO: update any cache you may have of the item's // rectangle/extent m_rect = rectPos; GetDocument()->UpdateAllViews(NULL); return TRUE; } void COCONCntrItem::OnGetItemPosition(CRect& rPosition) { ASSERT_VALID(this); // rPosition.SetRect(10, 10, 210, 210); if (m_rect.IsRectNull()) { CSize size; CClientDC dc(NULL); GetCachedExtent(&size, GetDrawAspect()); dc.HIMETRICtoDP(&size); m_rect = CRect(CPoint(10, 10), size); } rPosition = m_rect; }
Listing 31.3 shows how COCONCntrItem::Serialize must be modified to support serialization of the new member variable.
void COCONCntrItem::Serialize(CArchive& ar) { ASSERT_VALID(this); COleClientItem::Serialize(ar); if (ar.IsStoring()) { // TODO: add storing code here ar << m_rect; } else { // TODO: add loading code here ar >> m_rect; } }
Finally, the addition of the new Invalidate helper function (Listing 31.4) concludes changes to the implementation of COCONCntrItem.
void COCONCntrItem::Invalidate(CWnd *pWnd) { CRect rect = m_rect; rect.InflateRect(1, 1); pWnd->InvalidateRect(rect); }
Listing 31.5 shows the changes to COCONView::OnDraw; the new version takes into account the items' stored positions and draws them accordingly. It also draws all items (as opposed to just the current selection). Furthermore, this version of the function uses a CRectTracker object to highlight the currently selected item.
void COCONView::OnDraw(CDC* pDC) { COCONDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); POSITION pos = pDoc->GetStartPosition(); while (pos) { COCONCntrItem *pItem = (COCONCntrItem*)pDoc->GetNextClientItem(pos); pItem->Draw(pDC, pItem->m_rect); if (pItem == m_pSelection) { CRectTracker tracker; tracker.m_rect = pItem->m_rect; tracker.m_nStyle = CRectTracker::resizeInside | CRectTracker::solidLine; if (pItem->GetItemState() == COleClientItem::openState || pItem->GetItemState() == COleClientItem::activeUIState) tracker.m_nStyle |= CRectTracker::hatchInside; tracker.Draw(pDC); } } }
In the previous section, we already implemented highlighting the current selection. Adding support for selection of items using the mouse requires adding a handler for WM_LBUTTONDOWN events to the view class. This handler, shown in Listing 31.6, can be added using the ClassWizard.
void COCONView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default // CView::OnLButtonDown(nFlags, point); COCONDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); COCONCntrItem *pSelection = NULL; COCONCntrItem *pItem = (COCONCntrItem *)pDoc->GetInPlaceActiveItem(this); if (pItem != NULL) pItem->Close(); POSITION pos = pDoc->GetStartPosition(); while (pos) { pItem = (COCONCntrItem*)pDoc->GetNextClientItem(pos); if (pItem->m_rect.PtInRect(point)) pSelection = pItem; } if (pSelection != m_pSelection) { if (m_pSelection != NULL) m_pSelection->Invalidate(this); m_pSelection = pSelection; if (m_pSelection != NULL) m_pSelection->Invalidate(this); } }
Although the two areas of functionality are usually mentioned together, the requirements for a drag source application and for a drop target application are quite distinct. Of the two, implementation of a drag source is the easier task. However, both tasks are supported extensively in part by the OLE architecture, in part by the MFC.
The support for implementing a drag source comes in the form of the DoDragDrop member function of several classes, namely COleClientItem, COleServerItem, and COleDataSource.
Drop target functionality is supported through a series of member functions of the CView class; namely, its member functions OnDrop, OnDragEnter, OnDragOver, and OnDragLeave. However, unlike the DoDragDrop function, which can be called as is, these functions require customized implementations in your application.
With the help of the COleClientItem::DoDragDrop member function, adding drag source capability to an OLE container is almost embarrassingly simple. All we need to do is modify the handler function for WM_LBUTTONDOWN events, COCONView::OnLButtonDown. If and when a valid object is selected by the user using the mouse, we must call the object's DoDragDrop member function to perform the drag and drop operation. We must also perform a minor housekeeping chore: If the object was moved from our application to another, we must delete it from the list of objects maintained by our document class. However, with CDocItem-derived objects, this is also a very simple task; it is sufficient to simply delete the object using the delete operator. The CDocItem destructor function ensures that the object is properly removed from the document's list of objects.
The modified version of COCONView::OnLButtonDown is shown in Listing 31.7.
void COCONView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default // CView::OnLButtonDown(nFlags, point); COCONDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); COCONCntrItem *pSelection = NULL; COCONCntrItem *pItem = (COCONCntrItem *)pDoc->GetInPlaceActiveItem(this); if (pItem != NULL) pItem->Close(); POSITION pos = pDoc->GetStartPosition(); while (pos) { pItem = (COCONCntrItem*)pDoc->GetNextClientItem(pos); if (pItem->m_rect.PtInRect(point)) pSelection = pItem; } if (pSelection != m_pSelection) { if (m_pSelection != NULL) m_pSelection->Invalidate(this); m_pSelection = pSelection; if (m_pSelection != NULL) m_pSelection->Invalidate(this); } if (m_pSelection != NULL) { m_dragRect = m_pSelection->m_rect; if (m_pSelection->DoDragDrop(m_pSelection->m_rect, (CPoint)(point - m_pSelection->m_rect.TopLeft())) == DROPEFFECT_MOVE) { m_pSelection->Invalidate(this); delete m_pSelection; m_pSelection = NULL; } } }
Before we move onto the implementation of drop target functionality, I want to add a few notes concerning this drag source implementation.
Obviously, most applications have functionality above and beyond being an OLE container (if they implement container functionality at all). How would you implement drag source capabilities for those applications?
One possibility is to utilize the DoDragDrop member function of COleServerItem. If your application is an OLE server, it already has a COleServerItem-derived class defined to represent your application's document in an embedded item. Modify this class to support representation of only the current selection (as opposed to the entire document). This modification is easy; you need only to change the Serialize and OnDraw member functions and create a constructor that takes a parameter representing the current selection. The utility of this class goes beyond drag source functionality; it can also be used to represent linked items and to facilitate the transfer of the current selection to the clipboard.
Once your COleServerItem-derived class is complete, you can create an item of this type in your WM_LBUTTONDOWN handler (or wherever you wish to implement drag source functionality). Subsequently, you can utilize the member function COleServerItem::DoDragDrop to implement drag source capability just as simply as we did for OLE client items.
If you do not wish to use a COleServerItem-derived class for this purpose (for example, if your application is not an OLE server), you can also utilize the COleDataSource class. This class can be used to represent a selection for drag and drop and clipboard transfers. COleDataSource also has a DoDragDrop member function, so implementing drag source functionality using this class is equally simple.
Implementing an OLE drop target requires a lot more work than implementing a drag source. Several member functions of your view class require override versions. The view class must be registered as a drop target. Special considerations must be made to ensure that objects originating from within the application itself are handled properly and efficiently; for example, if the drag and drop operation effectively reduces to a move within the same window, it should be treated that way.
The set of CView member functions that require overrides is listed in Table 31.1.
Member Function |
Description |
OnDragEnter |
Called when an item is dragged into the window |
OnDragLeave |
Called when a dragged item leaves the window |
OnDragOver |
Called while an item is dragged within the window |
OnDrop |
Called when an item is released in the window |
Before we start writing code madly, here's a summary of exactly what we would like to see in our drop target application.
First, the obvious: If an object is released over our application's view window, we would like the object to appear at that location, preferably preserving its original size.
We would also like to see a tracking rectangle while the mouse is inside the view window. The rectangle will reflect the size of the object that is about to be dropped in the window.
Lastly, we would like to ensure that a drag and drop operation that reduces to merely moving an object within the same window is treated accordingly.
The implementation of the OnDragEnter, OnDragLeave, and OnDragOver function overrides requires a few additional member variables. These variables will be used to remember the drag rectangle's size and position and other drag characteristics during a drag operation. In addition, a member variable of type COleDropTarget is also required in order to register the view window as a drop target. The declaration of these variables should be added to the declaration of the view class as follows:
class COCONView : public CView { ... // Attributes public: ... COCONCntrItem* m_pSelection; COleDropTarget m_dropTarget; BOOL m_bInDrag; DROPEFFECT m_prevDropEffect; CRect m_dragRect; CPoint m_dragPoint; CSize m_dragSize; CSize m_dragOffset; ...
The declarations for the overrides of OnDragEnter, OnDragLeave, OnDragOver, and OnDrop should be added using the ClassWizard.
Our first task in making the application work as a drop target is to register it as one. This is accomplished by adding a member variable of type COleDropTarget and calling its Register member function at the appropriate moment of time. The most appropriate place for this is in the view class's OnCreate member function. To implement this registration, create a handler for WM_CREATE messages using the ClassWizard, and add the code shown in Listing 31.8.
int COCONView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1; // TODO: Add your specialized creation code here m_dropTarget.Register(this); return 0; }
The OnDragEnter, OnDragLeave, and OnDragOver member functions are used to manage visual feedback during a drag operation. OnDragEnter (Listing 31.9) attempts to retrieve the item's size by querying the item for the Object Descriptor clipboard type. This data type, when supplied, contains information about the transfer item including its size and the offset of the mouse pointer relative to the item's upper-left corner.
DROPEFFECT COCONView::OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) { // TODO: Add your specialized code here and/or call the base class // return CView::OnDragEnter(pDataObject, dwKeyState, point); ASSERT(m_prevDropEffect == DROPEFFECT_NONE); m_dragSize = CSize(0, 0); m_dragOffset = CSize(0, 0); HGLOBAL hObjDesc = pDataObject->GetGlobalData(cfObjectDescriptor); if (hObjDesc != NULL) { LPOBJECTDESCRIPTOR pObjDesc = (LPOBJECTDESCRIPTOR)GlobalLock(hObjDesc); ASSERT(pObjDesc != NULL); m_dragSize.cx = (int)pObjDesc->sizel.cx; m_dragSize.cy = (int)pObjDesc->sizel.cy; m_dragOffset.cx = (int)pObjDesc->pointl.x; m_dragOffset.cy = (int)pObjDesc->pointl.y; GlobalUnlock(hObjDesc); GlobalFree(hObjDesc); } CClientDC dc(NULL); dc.HIMETRICtoDP(&m_dragSize); dc.HIMETRICtoDP(&m_dragOffset); m_dragPoint = point - CSize(1, 1); return OnDragOver(pDataObject, dwKeyState, point); }
This function makes use of the global variable cfObjectDescriptor. Declare this variable at the top of your view class's implementation file as follows:
static cfObjectDescriptor = (CLIPFORMAT)::RegisterClipboardFormat(_T("Object Descriptor"));
The next member function is OnDragOver (Listing 31.10). This function is called every time the mouse moves while within the view window's client area. This function plays a dual role. First, it determines the currently applicable drop effect; based on the state of the Control, Shift, and Alt keys it determines whether the item, were it dropped in the window at this moment, should be copied, linked, or moved to this window.
DROPEFFECT COCONView::OnDragOver(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) { // TODO: Add your specialized code here and/or call the base class // return CView::OnDragOver(pDataObject, dwKeyState, point); DROPEFFECT de = DROPEFFECT_NONE; point -= m_dragOffset; if ((dwKeyState & (MK_CONTROL|MK_SHIFT)) == (MK_CONTROL|MK_SHIFT)) de = DROPEFFECT_LINK; else if ((dwKeyState & MK_CONTROL) == MK_CONTROL) de = DROPEFFECT_COPY; else if ((dwKeyState & MK_ALT) == MK_ALT) de = DROPEFFECT_MOVE; else de = DROPEFFECT_MOVE; if (point == m_dragPoint) return de; CClientDC dc(this); if (m_prevDropEffect != DROPEFFECT_NONE) dc.DrawFocusRect(CRect(m_dragPoint, m_dragSize)); m_prevDropEffect = de; if (m_prevDropEffect != DROPEFFECT_NONE) { m_dragPoint = point; dc.DrawFocusRect(CRect(point, m_dragSize)); } return de; }
The other role of this function is to actually draw visual feedback. This is accomplished by drawing a rectangle using the CDC::DrawFocusRect function.
The third function in this group is OnDragLeave (Listing 31.11). This function, the simplest of the three, is called to mark the end of a dragging operation.
void COCONView::OnDragLeave() { // TODO: Add your specialized code here and/or call the base class // CView::OnDragLeave(); CClientDC dc(this); if (m_prevDropEffect != DROPEFFECT_NONE) { dc.DrawFocusRect(CRect(m_dragPoint, m_dragSize)); m_prevDropEffect = DROPEFFECT_NONE; } }
Now it's time to turn our attention to the OnDrop member function (Listing 31.12). This function is by far the most important one in our implementation of drop target functionality. As its name implies, it is this function where the actual insertion of a dropped item takes place.
BOOL COCONView::OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) { // TODO: Add your specialized code here and/or call the base class // return CView::OnDrop(pDataObject, dropEffect, point); ASSERT_VALID(this); COCONDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CSize size; OnDragLeave(); CClientDC dc(NULL); point -= m_dragOffset; pDoc->SetModifiedFlag(TRUE); if ((dropEffect & DROPEFFECT_MOVE) && m_bInDrag) { ASSERT(m_pSelection != NULL); m_pSelection->Invalidate(this); m_pSelection->m_rect = m_dragRect + point - m_dragRect.TopLeft(); m_bInDrag = FALSE; return TRUE; } COCONCntrItem* pItem = NULL; TRY { pItem = new COCONCntrItem(pDoc); ASSERT_VALID(pItem); if (dropEffect & DROPEFFECT_LINK) { if (!pItem->CreateLinkFromData(pDataObject)) AfxThrowMemoryException(); } else { if (!pItem->CreateFromData(pDataObject)) AfxThrowMemoryException(); } ASSERT_VALID(pItem); pItem->GetExtent(&size, pItem->GetDrawAspect()); dc.HIMETRICtoDP(&size); pItem->m_rect = CRect(point, size); if (m_pSelection != NULL) m_pSelection->Invalidate(this); m_pSelection = pItem; if (m_pSelection != NULL) m_pSelection->Invalidate(this); } CATCH(CException, e) { if( pItem != NULL ) delete pItem; AfxMessageBox(IDP_FAILED_TO_CREATE); } END_CATCH return pItem != NULL; }
This function first terminates the drag operation by calling OnDragLeave and notifies the document that the contents are changing by calling CDocument::SetModifiedFlag.
Next, it makes an attempt to determine if the drag and drop operation actually represents moving an object within the same window. For this, we make use of the m_bInDrag member variable; this variable is set in COCONView::OnLButtonDown when a drag operation begins, as we see momentarily. If the operation is a move, the function simply updates the affected item's rectangle and returns.
In the case of a genuine drop operation, an attempt is made to create a new item of type COCONCntrItem using the drop data. If the attempt is successful, the item's size is determined and the item's rectangle is updated.
As I mentioned, the key to determining whether a drag and drop operation reduces to a mere move is the m_bInDrag member variable. To set this variable, we have to implement yet another modification to COCONView::OnLButtonDown. This final version of this function is shown in Listing 31.13.
void COCONView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default // CView::OnLButtonDown(nFlags, point); COCONDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); COCONCntrItem *pSelection = NULL; COCONCntrItem *pItem = (COCONCntrItem *)pDoc->GetInPlaceActiveItem(this); if (pItem != NULL) pItem->Close(); POSITION pos = pDoc->GetStartPosition(); while (pos) { pItem = (COCONCntrItem*)pDoc->GetNextClientItem(pos); if (pItem->m_rect.PtInRect(point)) pSelection = pItem; } if (pSelection != m_pSelection) { if (m_pSelection != NULL) m_pSelection->Invalidate(this); m_pSelection = pSelection; if (m_pSelection != NULL) m_pSelection->Invalidate(this); } if (m_pSelection != NULL) { m_bInDrag = TRUE; m_dragRect = m_pSelection->m_rect; DROPEFFECT dropEffect = m_pSelection->DoDragDrop(m_pSelection->m_rect, (CPoint)(point - m_pSelection->m_rect.TopLeft())); m_pSelection->Invalidate(this); if (m_bInDrag == TRUE && dropEffect == DROPEFFECT_MOVE) { delete m_pSelection; m_pSelection = NULL; } m_bInDrag = FALSE; } }
As you can see, the m_bInDrag member variable is set to TRUE just before calling COCONCntrItem::DoDragDrop. If during the drag a callback is made to the same view object, by looking at m_bInDrag we can determine that the source of the drag operation is the same view window.
The OnDrop member function resets m_bInDrag to FALSE if a drag operation is successfully reduced to a move. In this case, OnLButtonDown will not delete the selected item. That deletion is necessary otherwise, if the item was moved to a different window.
Finally, we need to initialize two of the member variables to ensure proper functioning of the view class. Listing 31.14 shows the initialization of m_bInDrag and m_prevDropEffect in the view class's constructor.
COCONView::COCONView() { // TODO: add construction code here m_pSelection = NULL; m_bInDrag = FALSE; m_prevDropEffect = DROPEFFECT_NONE; }
This concludes our construction of an OLE container supporting drag and drop. The completed application, shown in Figure 31.2, can serve as a drop target (or drag source) for word processor objects, spreadsheet cells, drawings, and many other types of OLE objects.
Figure 31.2. The OCON drag and drop application.
Drag and drop represents the capability to select items in one application (the drag source) and, using the mouse, drag the items and drop them in the window of another application (the drop target). Drag and drop, from the user's perspective, is a simpler mechanism for sharing data between applications than using the clipboard.
OLE provides extensive drag and drop support. This support is encapsulated in the MFC Library in the form of a series of drag and drop related classes and functions.
The implementation of a drag source is relatively easy. For this purpose, you can utilize the member functions of COleClientItem, COleServerItem, and COleDataSource.
Use of COleClientItem::DoDragDrop is recommended if the drag item is an embedded or linked OLE item represented by a COleClientItem-derived class. In this case, simply call the DoDragDrop function for the object that the user selected for dragging, and the framework does the rest. Remember to delete the object if the return value of DoDragDrop indicates that the selection has been moved (as opposed to copied or linked) to another application.
Use of COleServerItem::DoDragDrop is recommended for applications that are OLE servers. This function is most useful if your COleServerItem-derived class is already capable of representing a selection (as opposed to the entire contents of a document). Just create a COleServerItem-derived object representing the drag selection and use its DoDragDrop member function to perform the drag operation.
In applications that are not OLE servers, you can also consider using COleDataSource for implementing a drag source.
Implementing a drop target is a more involved operation. In addition to providing an override version of the OnDrop member function of your view class, you must also override the OnDragEnter, OnDragLeave, and OnDragOver member functions. The purpose of these functions, called by the framework while the mouse is over the view window during a drag and drop operation, is twofold: first, they are used to provide visual feedback during the drag; and second, they are used to inform the framework regarding the allowable drop operations.
In the simplest implementation of OnDrop, you can create a COleClientItem-derived object representing the drop item. In more involved implementations, you may consider inspecting the item and identify native data originating from within your own application, or data available in formats that your application can recognize.
In an application that acts both as a drag source and drop target, you should improve the application's efficiency by recognizing operations that reduce to a simple move. In this case, instead of removing the dragged item or items and creating new items, you can implement the operation as a simple position change.