VC++
MFC ActiveX Control Article, COM Drag - Drop
Example
|
|
Data
Transfer with Drag and
Drop
By Fritz Onion
Published in C++ Report, April 1999 issue.
The
ability to transfer information between applications on
the same computer, without either application having any
foreknowledge of the other, was one of the flagship
features of early window-based user interfaces. This means
of data transfer remains one of the most useful
user-oriented features of the Windows operating system
today. Drag and drop is a more recent incarnation of the
classic clipboard transfer, allowing users to transfer
data between applications by simply dragging between two
windows (no clipboard involved). This capability is built
on top of Microsoft COM
technology, and has become relatively easy to add to most
Visual C++ based applications.
This
article will look at the infrastructure behind the
drag
and drop feature of Windows, and describe how you can add
drag and drop to your applications using
MFC
helper classes. I
will also introduce a template helper class that
simplifies turning non-view windows into drop targets,
which is particularly useful for ActiveX controls built in
MFC.
There are
three sample projects that accompany this
article which
can be downloaded from here.
The first sample is a raw
C++ application that displays
text in a window and supports text-based drag and
drop
operations. The second sample implements the same
functionality using MFC, and the last
sample is an ActiveX
control built with MFC that subclasses an edit control and
supports text-based drag and
drop.
The Means
Data
transfer using drag and
drop is performed through a
standard COM interface called IDataObject .
This interface is advertised by objects that know how to
render data in one or more formats, and is used to clients
to retrieve that data. IDataObject is used in
several other places throughout OLE and ActiveX, so it is
very generic, and like most of these older interfaces, it
has some historical baggage associated with it. For the
purposes of this article, we are only really interested in
two of its methods:
interface IDataObject : IUnknown
{
HRESULT GetData(FORMATETC *pformatetcIn,
STGMEDIUM *pmedium);
HRESULT QueryGetData(FORMATETC *pformatetc);
// More methods not shown...
}
The
concept behind the interface is quite straight-forward. To
obtain data, a client simply calls IDataObject::GetData()
on the source object, which will populate some data
structure and return it to the client. What this data
looks like, depends on what parameters the client passed
into GetData() . In fact, before GetData()
is called, a client will typically ucancode.net the object if it
supports the data format that it is looking for. For example, a text processing application might understand
only text data, whereas an imaging application probably
understands bitmaps and metafiles. A client ucancode.nets this
question by using the IDataObject::QueryGetData()
method.
IDataObject* pDataObj = //obtain pointer somehow
// ucancode.net for text in a global buffer
FORMATETC formatEtc =
{CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
if (SUCCEEDED(pDataObj->QueryGetData(&formatEtc)) )
// data format is there - now retrieve it
This
query actually ucancode.nets two questions - do you have the data
in the format I am looking for, and can you provide it in
the storage medium I prefer? These questions are expressed
through the FORMATETC structure, which is the
only parameter to QueryGetData() . This
structure contains five fields, only two of which most
drag and drop operations care about - cfFormat
and tymed . The cfFormat field is
simply the clipboard format of the data which could be a
standard format (CF_TEXT , CF_BITMAP ,
...) or a custom registered application-specific format.
The tymed field describes the desired storage
medium (or media) for the transfer of data. It can be any
combination of the following bit-flags:
TYMED_HGLOBAL // Global buffer
TYMED_FILE // File name
TYMED_ISTREAM // IStream*
TYMED_ISTORAGE // IStorage*
TYMED_GDI // HBITMAP
TYMED_MFPICT // HMETAFILE
TYMED_ENHMF // HENHMETAFILE
It may
seem curious to make distinctions between so many
in-memory formats, but it is necessary in order to
properly reclaim the resource (HeapFree
versus DeleteObject for a global buffer and
an HBITMAP , for example).
Once the
client has found a data format he likes, he then requests
the data by calling the IDataObject::GetData()
method. For example, to retrieve the text data queried for
above, a client would write (using the same FORMATETC
structure):
STGMEDIUM stgMedium;
HRESULT hr = pDataObj->GetData(&formatEtc, &stgMedium);
if (SUCCEEDED(hr))
{
HGLOBAL gmem = stgMedium.hGlobal;
TCHAR* str = (TCHAR*)GlobalLock(gmem);
// use str
GlobalUnlock(gmem);
::ReleaseStgMedium(&stgMedium);
}
The STGMEDIUM
structure is a discriminated union for each of the media
types, using the TYMED value as the
discriminator. The API function ReleaseStgMedium()
releases the medium properly.
This
client-side code should give you an idea of what's
involved with providing IDataObject for a
data source. First of all, it is typically a stand-alone
COM object, independent of your primary data class. Most
data providers create an instance of a class that
implements IDataObject , provide that class
with a cache of the data necessary for client retrieval,
and send it off to deliver the data on its own.
As an
example, let's build a class that supports IDataObject
and is capable of providing access to a string buffer. The
class will simply contain a pointer to a string that will
be rendered on request.
class StrDataObj : public IDataObject
{
private:
ULONG m_cRef; // Reference count
TCHAR* m_strText;
public:
StrDataObj() : m_cRef(0), m_strText(0) {}
void SetText(TCHAR* text) { m_strText = text; }
// IUnknown and IDataObject methods would go here
};
A good
data provider will provide as many possible formats as it
can, to let the largest number of clients possible receive
its data. The two interesting methods of IDataObject
for a data provider are QueryGetData() and GetData() .
QueryGetData() should respond yes or no
indicating whether the format requested is available. As a
very simple data provider implementation, assume that we
only provide text in a global buffer. Our QueryGetData()
implementation would look like:
STDMETHODIMP
StrDataObj::QueryGetData(FORMATETC* pformatetc)
{
if ((pformatetc->dwAspect & DVASPECT_CONTENT) &&
(pformatetc->tymed & TYMED_HGLOBAL) &&
(pformatetc->cfFormat == CF_TEXT))
return S_OK;
return DV_E_FORMATETC;
}
And, the
actual data retrieval function might look like:
STDMETHODIMP StrDataObj::GetData(FORMATETC *pformatetcIn,
STGMEDIUM *pmedium)
{
if ((pformatetcIn->dwAspect & DVASPECT_CONTENT) &&
(pformatetcIn->tymed & TYMED_HGLOBAL) &&
(pformatetcIn->cfFormat == CF_TEXT))
{
void* pMem = HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(TCHAR)*(lstrlen(m_strText)+1));
lstrcpy((TCHAR*)pMem, m_strText);
pmedium->tymed = TYMED_HGLOBAL;
pmedium->hGlobal = pMem;
pmedium->pUnkForRelease = NULL;
return S_OK;
}
return DV_E_FORMATETC;
}
It is
important to note that unlike the name implies, IDataObject
is not a generic data transfer interface. It was designed
to transfer text or image data between applications, and
is not well suited for transferring large blocks of data
efficiently between applications (especially across
networks). Just as you wouldn't use the clipboard to
perform high-speed data transfer between applications,
don't use IDataObject either as it has many
of the same constraints as the clipboard, and is not
designed for efficiency out of process. For a better
solution to generic data transfer in COM, take a look at
conformant arrays.
The mechanism
We now
have the means for transferring the data, and are ready to
look at the mechanism of the drag and drop process.
Initiating a drag and drop operation is actually a bit
simpler than responding to one, so we will look at the
source side first. Given the IDataObject
implementation provided by our StrDataObj
class above, we want to be able to pass a pointer to that
object to some client, based on user mouse input. The user
should be able to click the mouse in our source
application's window, drag to another window (probably in
another application), and release, causing some data to
transfer (or copy if the control key is held down).
Most of
the mechanics of the drag and drop process are implemented
for you by the operating system. As a data source, you
simply provide two pointers - one to your data object, and
one to a drop source object which allows you to provide
feedback during the drag. This source object must
implement the IDropSource interface, and
should have a standard heap-based COM object lifetime. The
interface itself is quite simple, providing two methods
which will be called during the drag process:
interface IDropSource : IUnknown
{
HRESULT QueryContinueDrag (BOOL fEscapePressed,
DWORD grfKeyState);
HRESULT GiveFeedback (DWORD dwEffect);
}
QueryContinueDrag()
is called whenever the keyboard is touched, or a mouse key
is pressed during a drag, giving the source a chance to
abort the drag process. Typically a source will abort the
drag if the escape key was pressed, and perform a drop if
the left mouse button is released. The other method, GiveFeedback()
provides a means of changing the cursor based on what the
effect of the drag operation will be. For example, if the
drag is a copy, the cursor usually indicates so by
including a '+' sign as part of the icon. Most of the time
sources will want to use the default cursors which is
easily done by returning a special return code DRAGDROP_S_USEDEFAULTCURSORS
from this method. So a typical drop source implementation
will look like:
class DropSource : public IDropSource
{
private:
ULONG m_cRef; // Reference count
public:
DropSource() : m_cRef(0) {}
// IUnknown methods not shown
STDMETHODIMP QueryContinueDrag(BOOL fEscapePressed,
DWORD grfKeyState)
{
if (fEscapePressed)
return DRAGDROP_S_CANCEL;
if (!(grfKeyState & MK_LBUTTON))
return DRAGDROP_S_DROP;
return S_OK;
}
STDMETHODIMP GiveFeedback(DWORD dwEffect)
{
return DRAGDROP_S_USEDEFAULTCURSORS;
}
};
Now, to
actually initiate a drag and drop transfer, we need to
hand off our two pointers to the DoDragDrop()
function, typically in response to an WM_LBUTTONDOWN
message. Thus our handler might look like:
void OnLButtonDown(...)
{
DropSource* pDropSource = new DropSource;
pDropSource->AddRef();
DataObject* pDataObj = new DataObject;
pDataObj->AddRef();
pDataObj->SetText(/*some string in our app*/);
DWORD de;
HRESULT hr = DoDragDrop(pDataObj, pDropSource,
DROPEFFECT_COPY|DROPEFFECT_MOVE, &de);
if ((hr == DRAGDROP_S_DROP) &&
(de == DROPEFFECT_MOVE) )
// remove our string
pDataObj->Release();
pDropSource->Release();
}
The DoDragDrop()
method takes the IDataObject and IDropSource
pointers, plus what type of drop we want to support, and
an output parameter describing what type of drop occurred.
When invoked, DoDragDrop() takes over our
message loop in a modal fashion, returning only once the
drop has occurred or been aborted. When it takes over, it
tracks the mouse, making calls back into our IDropSource
interface, and checking the window under the current mouse
position to see if it is in fact a drop target. Once
complete, our source application should check to see if
the drop was a move or copy, and affect our local data
appropriately.
The other
side of the picture is the drop target implementation. To
act as a drop target, a client must provide a window that
has been registered with a drop target implementation (an
object supporting IDropTarget ). The IDropTarget
interface is called during a drag drop operation to notify
a potential drop target that a drop may be performed, and
to ucancode.net a drop target if it can in fact accept the drop. A
target application needs to build a COM class that
implements IDropTarget , and then register an
instance of that class with any window that wants to be a
drop target. To match our source class, let's build a
target class that accepts drops if they are text in global
buffers.
class DropTarget : public IDropTarget
{
private:
ULONG m_cRef; // Reference count
bool m_bCanAcceptDrop;
public:
DropTarget() : m_cRef(0), m_bCanAcceptDrop(false) {}
// IUnknown methods not shown
STDMETHODIMP DragEnter(IDataObject *pDataObj,
DWORD grfKeyState, POINTL pt, DWORD *pdwEffect);
STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt,
DWORD *pdwEffect);
STDMETHODIMP DragLeave();
STDMETHODIMP Drop(IDataObject *pDataObj,
DWORD grfKeyState, POINTL pt, DWORD *pdwEffect);
};
Notice
that in addition to the reference count, we added a
Boolean data member to our class to indicate whether we
can accept a drop or not. The first method of concern is DragEnter() .
This method is called during a drag operation whenever the
mouse cursor first enters our window (the one we've
associated with this target object). Our implementation
must check the incoming data object to see if it contains
data in a format we understand, and then indicate whether
a drop will occur or not by populating the pdwEffect
out parameter with the appropriate drop effect (checking
for the control key to see if it should be a copy or
move). It is also in this function that we will populate
our data member indicating whether or not we can handle
the current drop, which we will use later in our DragOver()
implementation.
STDMETHODIMP DropTarget::DragEnter(IDataObject* pDataObj,
DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT,
-1, TYMED_HGLOBAL};
if (FAILED(pDataObj->QueryGetData(&fmtetc)))
{
*pdwEffect = DROPEFFECT_NONE;
m_bCanAcceptDrop = false;
}
else
{
m_bCanAcceptDrop = true;
*pdwEffect = (grfKeyState & MK_CONTROL) ?
DROPEFFECT_COPY : DROPEFFECT_MOVE;
return S_OK;
}
Our next
method, DragOver() , will be called every time
the mouse moves over our window. This allows fine-grained
control over dropping at different locations in a window
if desired. Our implementation will simply populate the pdwEffect
field indicating whether or not the drop will occur, and
whether the drop will be a copy or a move based on the
control key.
STDMETHODIMP DropTarget::DragOver(DWORD grfKeyState,
POINTL pt, DWORD *pdwEffect)
{
if (!m_bCanAcceptDrop)
*pdwEffect = DROPEFFECT_NONE;
else if (grfKeyState & MK_CONTROL)
*pdwEffect = DROPEFFECT_COPY;
else
*pdwEffect = DROPEFFECT_MOVE;
return S_OK;
}
The DragLeave()
method is called when the mouse moves out of our window.
It simply provides us with a way of cleaning up any
resources we might have allocated in DragEnter .
Since we didn't allocate anything, our implementation
won't really do anything - although it will need to reset
our Boolean flag indicating that we will accept the
current drop. The last method, Drop() is
called when the user releases the mouse over our window,
and is where the actual data transfer occurs. Notice that
it takes an IDataObject pointer, which we
will use to extract our text data. We will also indicate
again whether the drop was a copy or a move.
STDMETHODIMP DropTarget::DragLeave()
{
m_bCanAcceptDrop = false;
return S_OK;
}
STDMETHODIMP Drop(IDataObject *pDataObj,
DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
{
FORMATETC fmtetc = {CF_TEXT, 0, DVASPECT_CONTENT,
-1, TYMED_HGLOBAL};
STGMEDIUM stgMedium;
HRESULT hr = pDataObj->GetData(&fmtetc, &stgMedium);
if (FAILED(hr)) return hr;
TCHAR* pStr = (TCHAR*)GlobalLock(stgMedium.hGlobal);
// save pStr text in our app somehow
GlobalUnlock(gmem);
::ReleaseStgMedium(&stgMedium);
*pdwEffect = (grfKeyState & MK_CONTROL) ?
DROPEFFECT_COPY : DROPEFFECT_MOVE;
return S_OK;
}
The last
detail to be taken care of in the target side is to
associate an instance of our DropTarget class with a
window, turning it into a drop target. This is done with
the RegisterDragDrop method, which takes an HWND
and an IDropTarget pointer, and actually
caches the IDropTarget pointer in the
extended bits of the window handle. This allows the
dragging operation to easily check and see if any given
window handle is in fact a drop target, and if it is, to
extract the IDropTarget interface pointer and
make the appropriate calls. This association is often made
when the window itself is created, usually in a handler
for the WM_CREATE message.
void OnCreate()
{
DropTarget* pDropTarget = new DropTarget;
pDropTarget->AddRef();
HWND hWnd = // grab our window handle somehow
RegisterDragDrop(hWnd, pDropTarget);
pDropTarget->Release();
}
One other
detail that should be mentioned is that since both the
data source and drop target applications will be using COM, they must initialize the
COM libraries. This is
typically done by calling CoInitialize() at
the beginning of the process, but if drag and drop
features are being used, you must call OleInitialize()
instead, which will set up the additional runtime
infrastructure needed to execute drag and drop operations.
The corresponding cleanup method is OleUninitialize() .
int WinMain(...)
{
if (FAILED(OleInitialize(0)))
return -1;
// Main code and message loop would go here
OleUninitialize();
}
Help From MFC
Although
building support for drag and drop from scratch is not too
difficult, MFC does simplify things by providing a couple
of wrapper classes that make some common assumptions about
the way you will provide drag and drop support in your
application. One of the most useful classes is its COleDataSource
class. This class provides a full implementation of the IDataObject
interface, and allows you to populate its internal list of
data structures with as many data types as you want to
support in your drop operation with its cache methods.
class COleDataSource : public CCmdTarget
{
public:
void CacheGlobalData(CLIPFORMAT cfFormat,
HGLOBAL hGlobal, LPFORMATETC lpFormatEtc = NULL); void CacheData(CLIPFORMAT cfFormat,
LPSTGMEDIUM lpStgMedium, LPFORMATETC lpFormatEtc);
DROPEFFECT DoDragDrop(...);
// More functions not shown
};
Notice
that it also provides a wrapper around the DoDragDrop()
method, which implicitly passes this instance of the data
source and a default implementation of IDropSource
(an instance of MFC's COleDropTarget class)
to the API call. Thus the process of sourcing a drag and
drop operation using MFC can be as simple as:
void CMyWnd::OnLButtonDown(...)
{
COleDataSource ds;
// Assume m_String is a CString data member of this
// class containing the string data to transfer
void* pMem = HeapAlloc(GetProcessHeap(),
0, sizeof(TCHAR)*(m_String.GetLength()+1));
lstrcpy((TCHAR*)pMem, m_String);
ds.CacheGlobalData(CF_TEXT, pMem);
ds.DoDragDrop(DROPEFFECT_COPY|DROPEFFECT_MOVE);
}
The COleDataSource
class' implementation of DoDragDrop() will
actually not initiate the drop operation until the user
moves out of a bounding rectangle, which means that
trivial mouse clicks will not start a drag operation - the
user must really drag before the operation begins. This
class also provides a way of providing the data through a
callback instead of caching it. This is a good idea if you
have a large amount of data, or your have a large number
of data formats, which really shouldn't be cached unless
the user actually drops the data somewhere. This is
achieved by deriving a new class from COleDataSource ,
overriding OnRenderData() to receive the
notification, and use the DelayRenderData()
method to indicate data is available instead of the cache
methods.
If you're
trying to turn a CWnd -derived class into a
drop target, MFC provides the COleDropTarget
class to help. In fact, this class assumes that you will
usually add drop target support to your view class (CView -derived),
so its implementation is to simply forward each of the
four notification methods of IDropTarget onto
the view class it is associated with (if it is in fact
associated with a view). For example, the DragEnter()
method implementation looks like:
DROPEFFECT COleDropTarget::OnDragEnter(CWnd* pWnd,
COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point)
{
if (!pWnd->IsKindOf(RUNTIME_CLASS(CView)))
return DROPEFFECT_NONE;
CView* pView = (CView*)pWnd;
return pView->OnDragEnter(pDataObject,
dwKeyState, point);
}
Notice
that if the incoming window pointer is not a view, it will
return no drop effect, otherwise it forwards the request
on to the method of the same name in the view class. This
makes turning your view class into a drop target quite
straight forward - add a data member to your view class of
type COleDropTarget , override the four
notification methods in your view class, and call the
register method of the COleDropTarget class
when your view is first created, as shown below:
class CTextEditView : public CView
{
private:
bool m_bCanAcceptDrop;
COleDropTarget m_DropTarget;
public:
virtual DROPEFFECT OnDragEnter();
virtual DROPEFFECT OnDragOver();
virtual void OnDragLeave();
virtual BOOL OnDrop();
int OnCreate(LPCREATESTRUCT lpCreateStruct)
{
CView::OnCreate(lpCreateStruct);
m_DropTarget.Register(this);
return 0;
}
//...
};
The
implementation of the four notification functions would
essentially be identical to our earlier IDropTarget
implementation, so I won't bother to show them again. For
the full implementation, please refer to the online
samples mentioned earlier.
A More Generic Drop
Target
Although
being able to turn your view class into a drop target is
often what you want to do, there are occasions when you
may want to turn non-view windows into drop targets as
well. For example, adding drag and drop support to an
ActiveX control is often desirable. Given the
implementation of COleDropTarget , the only
obvious way to provide drop target support to a non-view
window is to derive a new class from COleDropTarget ,
and somehow forward the four notifications to your window
by hand. A slightly more generic approach, however, is to
define a template-based forwarding class derived from COleDropTarget
that could be used in any CWnd -derived class.
This class will assume that the CWnd -derived
class it is being registered with will provide the four
notification methods of IDropTarget as
non-virtual methods with the same signature as the methods
in COleDropTarget , and simply forward the
notifications to the class.
template
class CForwardingTarget : public COleDropTarget
{
private:
T* m_pForward;
public:
CForwardingTarget() : m_pForward(NULL){}
BOOL Register(T* pForward)
{
m_pForward = pForward;
return COleDropTarget::Register(pForward);
}
virtual DROPEFFECT OnDragEnter(...)
{ return m_pForward->OnDragEnter(...); }
virtual DROPEFFECT OnDragOver(...)
{ return m_pForward->OnDragOver(...); }
virtual BOOL OnDrop(...)
{ return m_pForward->OnDrop(...); }
virtual void OnDragLeave(...)
{ m_pForward->OnDragLeave(pWnd); }
};
As an
example use of this new class, consider an ActiveX control
built in MFC to which we would like to add drop target
support. We would simply add a data member of our new
class passing our class type in to instantiate the
template, and provide implementations of the four
notification methods. Then in our WM_CREATE
handler, we would register the drop target:
class CMyControl : public COleControl
{
private:
CForwardingTarget<CMyControl> m_DropTarget;
int OnCreate(LPCREATESTRUCT lpCreateStruct)
{
COleControl::OnCreate(lpCreateStruct);
m_DropTarget.Register(this);
return 0;
}
DROPEFFECT OnDragEnter(?;
DROPEFFECT OnDragOver(?;
BOOL OnDrop(?;
void OnDragLeave(CWnd* pWnd);
};
Again,
for the complete implementation of an ActiveX control with
drop target support in MFC, please refer to the online
samples.
Summary
Drag and
drop is a useful way of transferring data between
applications in Windows, and should be added to any
application that works with data that might be
recognizable by any other application (most commonly text
and images). The flexible interfaces that define this
operation allow for a variety of implementations,
including drag and drop within a single application, or
even within a single window. MFC provides wrapper classes
for drag and drop operations, and makes turning your view
class into a drop target quite straight-forward, and with
the generic forwarding class presented, it is equally easy
for any CWnd -derived class. I hope this
article encourages you to start dragging and dropping
today!
|