Introduction
There are many Windows C++ programming books that contain chapters on how to use the clipboard and drag-and-drop. Some show how to perform these operations with the basic Windows API, some use MFC, and others use COM/OLE to do the job.
However, when I wanted to start doing clipboard operations of my own, I discovered that all of these sources had one limitation in common: They usually only dealt with plain text (CF_TEXT
) transfers, but I had to handle my own special objects.
Then, I read an article by Keith Rule, showing the use of CMemFile
and CSharedFile
objects to transfer text blocks via serialization. I decided that this process could be expanded to work on any class which was derived from CObject
, as long as the class in question had its own Serialize()
method.
The result is the drag-drop manager described in this article.
Target Audience
This article is intended for other MFC programmers who wish to implement drag-and-drop operations in document-view architecture applications. Basic familiarity with the document-view architecture is assumed. No knowledge of OLE or COM is needed in order to use the class described here.
Acknowledgments
- The idea for using serialization to transfer the data to and from the buffers came the article Basic Copy/Paste and Drag/Drop Support by Keith Rule
- The general strategy for event handling with drag and drop came from Chapter 1 of The Essence of OLE with Active X by David S. Platt.
The Drag-Drop Manager Class - CDragDropMgr
Requirements
I was planning to use the class in a standard SDI graphics drawing program. It would be necessary to:
- Copy graphics objects (shapes and lines, generally) out to the clipboard when cut or copied.
- Paste these objects back into the view window.
- Drag a selected object around in the view to a new location.
- Drag an object into another view window and drop it into place.
- As well as the special graphics objects used in the application, it would also be necessary to let a user select blocks of text from other applications and either copy/paste them into the graphics program using the clipboard, or drag them directly into the view.
There was no requirement for the user to be able to turn graphics objects into plain text for pasting or dragging into other applications.
Environment
The manager class is designed to be created and used by a view class, usually derived from CView
or CScrollView
. In an MDI-type application, each view creates its own manager class.
It is the owning view's responsibility to communicate changes back to the CDocument
class that controls it, and to any other views that might be affected by the change. For example, if a given view accepts the drop of a "square" object, it must tell its owning document to add the new shape, and then tell the other views to update themselves to display the new object.
The manager class uses OLE and MFC. It was compiled and tested using Visual Studio 5.0, on both Windows NT 4.0 and Windows 95.
Inheritance
The manager class is not derived from any other classes.
Static or Global Variables in ddmgr.h
static BOOL g_bOleInitCalled = FALSE;
This variable is used to indicate whether this manager or some other manager in the same application has initialized OLE already.
Project Notes
No special project settings are needed for compilation or link editing. It was compiled successfully under warning level 3.
Public Methods
Constructor
CDragDropMgr(BOOL bInitOle = TRUE);
The constructor sets up the OLE environment by calling InitOle()
, if the caller indicates that this needs to be done. It is not necessary for the parent application to initialize OLE itself. If it has done so, then it should either call the constructor with the flag set FALSE
, or set the g_bOleInitCalled
flag TRUE
before creating any manager objects.
There is an annoying debug assertion failure if you try to initialize OLE when it has already been initialized. This function is an attempt to minimize its effects.
MakeDropTarget
void MakeDropTarget(CView* pView);
The caller uses this function to register itself as a valid OLE drop target.
AddFormat
BOOL AddFormat(CString csFormat);
The caller uses this function to create the list of object formats which the manager object should recognize.
If csFormat
is set to "CF_TEXT
" (note that this is a string
, not the UINT CF_TEXT
format value), then CF_TEXT
type data can be accepted (but cannot be copied out).
OkToPaste
BOOL OkToPaste();
This method returns TRUE
if a recognized data type is on the clipboard and available for pasting. It is not used for drag-drop operations. It is really just a shorthand version of the AvailableDataType()
method described below.
AvailableDataType
CString AvailableDataType(COleDataObject* pDataObject);
This method returns the string
version of the data type contained in the OLE data object referenced by the caller. If pDataObject
is NULL
, then the method assumes that the caller wants to check the clipboard buffer.
The method returns either a string
with the data type, or an empty string
if the data is not recognized by the manager.
PrepareDrop
BOOL PrepareDrop(BOOL bToClipboard,
LPCTSTR lpstrFormat,
CObject* pObj,
DROPEFFECT* pDropEffect);
This function is used to copy a CObject
-derived object out to the clipboard or to an OLE data object for dragging.
The caller passes in:
bToClipboard
: A flag stating whether the target object should be sent to the clipboard or readied for drag-drop lpstrFormat
: The format type to use (a pointer to a character string
) pObj
: A pointer to the target object, which must have its own Serialize()
method. pDropEffect
: A pointer to the DROPEFFECT
enumeration which OLE uses. The caller can set this NULL
if copying to the clipboard, but it may not be NULL
if preparing a drag-drop.
You usually call this function from:
- Your view class's
OnLButtonDown()
event handler when starting a drag-drop. - Your view (or document) class's
OnEditCopy()
and/or OnEditCut()
event handler when copying data to the clipboard or deleting an object.
DoDrop
BOOL DoDrop(CObject* pO,
COleDataObject* pDataObject,
LPCTSTR lpstrFormat);
This method is the inverse of PrepareDrop()
. It copies data from the clipboard or drag-drop buffer into a target object for use by the caller.
It is the callers' responsibility to create the target object and make sure it has its own version of the Serialize()
method.
The caller passes in:
pO
: A pointer to an object to be filled with data, pDataObject
: A pointer to the OLE data object containing the data. If this pointer is set NULL
, it means that the data should come from the clipboard. lpstrFormat
: A pointer to the data format name.
You usually call this function from:
- Your view class's
OnDrop()
event handler when a drag-drop operation ends, to load the data for use. - Your view (or document) class's
OnEditPaste()
method when doing a paste from the clipboard.
OnDragOver
DROPEFFECT OnDragOver(COleDataObject* pDataObject,
DWORD dwKeyState,
CPoint point);
This method is called from the caller's OnDragOver()
event handler.
It returns the type of drag which is happening:
If the user is just dragging a valid object, it returns DROPEFFECT_MOVE
.
If the user is dragging a valid object with the control key depressed, it returns DROPEFFECT_COPY
. If desired, the caller can then create a new copy of the object at the drop location, rather than moving the object.
The caller just passes in the parameters handed to it in its own OnDragOver()
event handler.
OnDragEnter
BOOL OnDragEnter(COleDataObject* pDataObject);
Call this method from your view class's OnDragEnter()
event handler.
It returns TRUE
if the passed-in OLE data object contains data the manager recognizes, otherwise FALSE
.
GetCFText
BOOL GetCFText(CStringArray* pcsText,
COleDataObject* pDataObject);
This is a specialized method used in place of DoDrop()
to handle incoming CF_TEXT
data.
The method fills the passed-in CStringArray
with the lines of text contained in the data buffer. If pDataObject
is NULL
, this means the data is to be gotten from the clipboard buffer.
Protected Methods
InitOle()
BOOL InitOle();
This method sets up the OLE environment for your application. It is called from the manager's constructor.
Public Properties
None.
Protected Properties
BOOL m_bOkToDrop;
Used to indicate that there is a "legal" object available to be dropped.
COleDropTarget m_DropTarget;
Used to register the "owning" view:
CStringArray m_csFormats;
Used to store the text string
names of registered clipboard data formats that the owning view is able to handle.
CArray<UINT,UINT> m_nFormats;
Parallel array of UINT
values associated with registered format strings.
Using the Manager in a Sample Application - "TextDemo"
The description above is probably not enough to let you actually use the class. For that purpose, an example application is probably the best illustration.
The TextDemo App
The textdemo
application shows how to use the drag-drop manager class. It also shows the use of a basic text-block object entry class. This class will be the subject of another article, so it is not explained here in detail.
The textdemo
application is intended merely as a framework for demonstrating the drag-drop manager and basic text block objects. Therefore it is pretty simple, and the user interface is rather crude.
Textdemo
also does a lot of housekeeping that has nothing at all to do with dragging and dropping objects: It must keep track of the objects, re-draw them when appropriate, handle selection of objects, etc. This article does not cover any of these subjects. It is likely that you will not care for the way I have implemented the logic for these tasks; hopefully, you will recognize that you can design your own applications as desired and still use the drag-drop manager class.
Textdemo
lets you create text blocks by typing on the screen (draw|text blocks menu option), create gray squares by clicking on the screen (draw|squares menu option), or select objects by entering "select mode" (draw|select objects menu option).
Textdemo
uses two different object types in order to show how to use the drag-drop manager to handle more than one object type.
When you have selected an object, you can copy it to the clipboard, paste it into the view (it appears in the upper-left corner), or delete it (it gets cut to the clipboard).
If you drag a selected object, you can move it to a new location in the view, or drag it across the desktop to another instance of textdemo.
You can also select a block of plain text from another application, copy it to the clipboard, and then paste it into textdemo
(it gets turned into a text block object), or drag the selected text directly into textdemo and drop it where desired.
If you hold down the control key while dragging, a copy of the selected object is created.
The sections below detail what view class methods to create in order to use the drag-drop manager class. I gathered these functions into the view class merely for convenience. There is no reason you could not handle some of them in the document class if that seems more appropriate to you.
Textdemo
uses a few defines, in globals.h:
#define MY_TXTBLK "CTxtBlk"
#define MY_SQUARE "CSquare"
#define PLAIN_TXT "CF_TEXT" //Special for ddmgr class
Preparing and Freeing the Manager
In the constructor for the view class, create the manager object for this view to use, and give it the list of legal object types:
CTextDemoView::CTextDemoView()
{
. . .
m_pDDMgr =
new CDragDropMgr; m_pDDMgr->AddFormat(MY_TXTBLK);
m_pDDMgr->AddFormat(MY_SQUARE);m_pDDMgr->AddFormat(PLAIN_TXT);
. . .
}
In the destructor, free the manager object:
CTextDemoView::~CTextDemoView()
{
if (m_pDDMgr != NULL)
delete m_pDDMgr;
}
Message (Event) Handlers
OnCreate
In the OnCreate()
event handler for the view class, register this view as a drop target. This must be done here instead of in the constructor because the view is not valid itself until this point:
int CTextDemoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
. . .
if (m_pDDMgr ==
NULL) return
-1;m_pDDMgr->MakeDropTarget((CView*)this);
. . .
}
OnLButtonDown
If the curent action is "select objects", see if an object has been selected. If an object has been selected, always copy its data out to the drag-drop buffer via PrepareDrop()
. If the manager indicates that the selected object is being moved, delete that object from the app (its data is safe in the OLE buffer):
void CTextDemoView::OnLButtonDown(UINT nFlags, CPoint point)
{
. . .
if(pDoc->CurrentAction() == ACTION_SELECT)
{
m_pTextList->HideCaret(this);
StartSelectAction(point);
}
. . .
}
void CTextDemoView::StartSelectAction(CPoint point)
{
CTextDemoDoc* pDoc = GetDocument();
FindSelection(point);
if (pDoc->AnObjectIsSelected())
{
DROPEFFECT de = DROPEFFECT_NONE;
if (m_pDDMgr->PrepareDrop(DO_DRAGDROP,
DDFormat(),
pDoc->SelObjectPtr(),
&de))
{
pDoc->CheckAndSetCursor();
SetMoving(de == DROPEFFECT_MOVE);
if (MovingAnObject())
pDoc->DeleteSelectedObject();
}
}
}
OnDragOver
This event fires every time the mouse moves during a drag operation. Simply call the manager object's OnDragOver()
method to let it handle things. The method returns the type of drag (move or copy) taking place:
DROPEFFECT CTextDemoView::OnDragOver(
COleDataObject* pDataObject,
DWORD dwKeyState,
CPoint point)
{
DROPEFFECT de = m_pDDMgr->OnDragOver(pDataObject,
dwKeyState,
point);
SetMoving(de == DROPEFFECT_MOVE);
return de;
}
OnDrop
This event fires when the user drops data into the view.
The event code simply determines what type of data is out there, and if legal, performs logic to:
- Create a new instance of the object
- Load it with the data from the buffer via
DoDrop()
- Place the new object into its proper spot in the view:
BOOL CTextDemoView::OnDrop(COleDataObject* pDataObject,
DROPEFFECT dropEffect,
CPoint point)
{
CString csF = m_pDDMgr->AvailableDataType(pDataObject);
if (csF == MY_TXTBLK)
return DropTextBlock(pDataObject,dropEffect,point);
else if (csF == MY_SQUARE)
return DropSquare(pDataObject,dropEffect,point);
else if (csF == PLAIN_TXT)
return DropCFText(pDataObject,dropEffect,point);
return FALSE;
}
Only the code for DropSquare
is shown here; the other two functions use essentially the same logic:
BOOL CTextDemoView::DropSquare(
COleDataObject* pDataObject,
DROPEFFECT dropEffect,
CPoint point)
{
CClientDC dc(this);
CTextDemoDoc* pDoc = GetDocument();
OnPrepareDC(&dc);
dc.DPtoLP(&point);
CSquare* pS = new CSquare(CPoint(0,0));
if (pS == NULL)
return FALSE;
pDoc->SetModifiedFlag(TRUE);
if (!m_pDDMgr->DoDrop(pS,
pDataObject,
MY_SQUARE))
{
AfxMessageBox("Drop failed");
pDoc->DeleteLastSquare();
return FALSE;
}
pS->SetLocationTo(point);
pDoc->m_SquList.AddTail(pS);
if (dropEffect == DROPEFFECT_MOVE)
SetMoving(FALSE);
RedrawArea(pS->BoundingRect());
return TRUE;
}
OnDragEnter
This event fires when the user first drags an object into the view.
Simply call the manager's OnDragEnter()
method to let it set things up:
DROPEFFECT CTextDemoView::OnDragEnter(
COleDataObject* pDataObject,
DWORD dwKeyState,
CPoint point)
{
m_pDDMgr->OnDragEnter(pDataObject);
return this->OnDragOver(pDataObject,dwKeyState,point);
}
OnDragLeave
No action is needed for this handler.
OnEditCopy, OnEditCut
These events fire when the user copies or deletes an object (Your own event handlers for these actions may be different). In both cases, copy the selected object to the clipboard:
void CTextDemoView::OnEditCopy()
{
CopySelectedObjectToClipboard();
}
void CTextDemoView::OnEditCut()
{
CopySelectedObjectToClipboard();GetDocument()->DeleteSelectedObject();
}
void CTextDemoView::CopySelectedObjectToClipboard()
{
CTextDemoDoc* pDoc =
GetDocument(); if(!pDoc->AnObjectIsSelected())
return;
DROPEFFECT drop =
DROPEFFECT_NONE;
m_pDDMgr->PrepareDrop(DO_CLIPBOARD,
DDFormat(), pDoc->SelObjectPtr(),
&drop);
}
OnUpdateEditPaste
This event enables the "paste" edit menu option if the manager indicates that it has valid data for pasting:
void CTextDemoView::OnUpdateEditPaste(CCmdUI*pCmdUI)
{
pCmdUI->Enable(m_pDDMgr->OkToPaste()
&&!GetDocument()->UserIsDrawingText());
}
OnEditPaste
This event fires when the user pastes data into the view from the clipboard.
void CTextDemoView::OnEditPaste()
{
CTextDemoDoc* pDoc = GetDocument();
CString csF = m_pDDMgr->AvailableDataType(NULL);
if (csF == MY_TXTBLK)
DropTextBlock(NULL,DROPEFFECT_NONE,CPoint(0,0));
else if (csF == MY_SQUARE)
{
CPoint pt(SQ_HALFSIDE,SQ_HALFSIDE);
GetDC()->LPtoDP(&pt);
DropSquare(NULL,DROPEFFECT_NONE,pt);
}
else if (csF == PLAIN_TXT)
DropCFText(NULL,DROPEFFECT_NONE,CPoint(0,0));
DeselectCurrentObject();
}
Limitations
- When serializing from the buffer, the clipboard and OLE data objects do not seem to recognize versionable schemas. If you look at the code in the
CSquare
class, for example, you will see that I've gotten around it by using my own rather crude schema method. - The manager does not allow more than 1000 characters of
CF_TEXT
data to be imported. You can change this if desired, of course. - The manager has no methods to let you create
CF_TEXT
data. It should not be difficult to extend the manager to handle this case, however: You just need to write a "PrepareDrop
" method to put the data out in the CF_TEXT
format. How the data comes into the class is the real issue to consider. This will depend on what your parent app is doing.
Conclusion
It is my hope that you have found this class of interest, and that you will be able to use it (or change it around to suit your own needs) in any drawing programs you may develop.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.