Environment: The only restriction is that you have IE4 (with at least SP2)
Introduction
Many people have been asking for Drag & Drop between windows controls.
After reading an article on the CodeGuru site, and trying the functionality
of MFC for drag and drop, I decided to implement it in to one of my applications.
The MFC implementation is a very good example of the quality of code that is
written after after lengthy heavy technical discussion. In other words, it’s
bad. It has too much overhead on the OO site, and in the end it doesn4t
work (and when it does, it’s a miracle). If you don’t agree with this
statement, step through the MFC code to see what I mean.
This is also a rather weird thing. Microsoft has been promoting Drag&Drop
API (D&D API) as one of the major things to implement in a Windows application.
Yet, Microsoft doesn’t even support it in most if its own applications.
Support for it is good on the COM side of things, but how do you implement
Drag & Drop from a standard Win32 or MFC application?
I have learned a lot from CodeGuru over the years. So, here’s is something back.
The secret lies buried in a COM interface called IDropTarget.
How does it all work?
So how does it all work? Well, what we basically do is register the
windows we want to use in drag and drop operations with the OS and
the D&D API. When a modal drag and drop operation is present, the D&D
API will scan that list and if it finds that the cursor is in one the
the windows registered, it will notify that window by the interface
object IDropTarget. That object is then responsible for handling the
result of the drag and drop.
So we link the object up with the window [handle] that will support a
drop operation, and call the standard MFC drop API
COleDataSource::DoDragDrop(). Below is some documentation on how
to do this, the sample distributed with this article shows how to
drag and drop text from a listbox, a combobox, and a button between
each other. Implementing this for other controls is not that hard.
IDropTarget
IDropTarget is defined in oleidl.h, one of the basic OLE include
files. We will implement this interface in a baseclass called
ECOleMemDropTarget. This class will handle the calls send by the
Drag and Drop API. The class will look for dropped data and check
if the type of data passed to it, and registered by the D&D API on
the clipboard, is correct. If that is the case, it will let us
manipulate it.
The definition for ECOleMemDropTarget looks like this.
interface ECOleMemDropTarget : public IDropTarget { public: ECOleMemDropTarget(); ~ECOleMemDropTarget(); // basic IUnknown stuff HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void ** ppvObject); ULONG STDMETHODCALLTYPE AddRef(void); ULONG STDMETHODCALLTYPE Release(void); IDropTarget stuff HRESULT STDMETHODCALLTYPE DragEnter(struct IDataObject *,unsigned long,struct _POINTL,unsigned long *); HRESULT STDMETHODCALLTYPE DragOver(unsigned long,struct _POINTL,unsigned long *); HRESULT STDMETHODCALLTYPE DragLeave(void); HRESULT STDMETHODCALLTYPE Drop(struct IDataObject *,unsigned long,struct _POINTL,unsigned long *); // called by parents BOOL Register(CWnd* pWnd, UINT pDataType); void Revoke(); // call parent we have goodies virtual void GotDrop(void); virtual DWORD GotDrag(void); virtual void GotLeave(void); virtual DWORD GotEnter(void); public: BYTE *m_Data; CPoint m_DropPoint; DWORD m_KeyState; protected: CWnd* m_DropTargetWnd; UINT m_RegisterType; DWORD m_dwRefCount; };
It inherits from IDropTarget, and implements the needed methods from
IUnknown as all COM interface derived classes, and the m_dwRefCount
datamember needed for garbage collection/reference counting.
DragEnter, DragOver, DragLeave and Drop are interface methods of
IDropTarget.These are the methods that will get called by the D&D
API when a drag and drop operation is in progress above our window.
We don4t define these as pure virtual functions. Some of our parent
will not be interested in what we have to say (yeah, C++ is really
like the world out there ;-)), and we dont want to put the burden
on them of writing some stub code.
The public datamembers contain data on a D&D call in the works:
m_DropPoint contains the screen coordinates of the point of the
drop, and m_KeyState the state of some windows key when the user
released the mouse. You could edit the functionality of the class
so that these contain valid data during the D&D operation.
The code of our class
First are the Register and Revoke members. These will set up a
window as a drag and drop client.
BOOL ECOleMemDropTarget::Register(CWnd* pWnd, UINT pDataType) { if(NULL == pWnd) return E_FAIL; if(0L == pDataType) return E_FAIL; // keep m_DropTargetWnd = pWnd; m_RegisterType = pDataType; // this is ok, we have it DWORD hRes = ::RegisterDragDrop(m_DropTargetWnd->m_hWnd, this); if(SUCCEEDED(hRes)) return TRUE; // wont accept data now return FALSE; }
We pass the MFC window handle for our window, and the datatype of
the data that will be registered on the clipboard and in what we
are interested. The code then calls Windows RegisterDragDrop, and
that passes our window WIN32 handle to the OS as a valid target for
Drag&Drop. Note that we must already have a window handle here, so
this call will not succeed from the constructor of window classes.
Mission part one ok. Revoke just calls Windows RevokeDragDrop().
This removes the window handle again from the OS D&D API.
Your mission, should you…
To make our code a bit more generic, I derived a class from
ECOleMemDropTarget called ECMaterialDropText. Thats a basic class
that will support drag and drop for text, and that will handle
the text data type from the clipboard.
We need to connect an instance of this class to a window control.
The easiest thing to do is just inherit our own custom controls
for this from MFC. Lets do this for a button. We bring up the
classwizard, press “Add class… new”, enter the name ECButton
(our whatever you want to call it), and select CButton as a baseclass.
OK, and we have our own custom button class. God, don4t we all love
that wizard.
We will put a datamember instance of ECOleMemDropTarget in our button
class: ECMaterialDropText *m_TargetDrop. In the constructor we create
the memory for it. We call the Register() function of the member.
Because MFC doesn4t call any Create members, we will do this in a
small methode called InitDrag(). That basically just calls our
Register() of ECMaterialDropText.
Next, we add handlers for mousebuttons, mousemove4s and a timer
function. We use the left mouse button, you could use the right,
middle or the n4th mouse button. OnLButtonDown keeps our coordinates
and starts the timer, OnLButtonUp stops the timer. The OnTimer method
checks if the mouse cursor is still in the window.
Why do we do all this? Well, we only want to start a drag and drop
if the user has pressed and moves the mouse in the window (I choose
a distance of 10 pixels). The timer checks if that is the case, and
if the mouse is outside the button (and a drag and drop not started),
it cancels the action. This also prevents a user from pressing the
mouse button outside of our window control, and the moving and releasing
inside of the control. Ah users, difficult people. The world would be
so good without them 🙂
The real action for the drag and drop is started in OnMouseMove.
If we did the minimum distance we take the text from the control,
and pump it to the COleDataSource to start a drag. That part is
described in other good articles in the Clipboard area on this
great site. And DoDragDrop() starts our action.
Action!
The Drag & Drop API will now modally begin scanning our mouse moves
and the windows the mouse is in. If it arrives in our button or one
of the registered other drag and drop windows, the API will call the
IDropTarget::DragEnter member. We call GotEnter() for our parents,
in our case for ECMaterialDropText::GotEnter().
DragEnter must set a constant giving GUI feedback on the action
the user can do. For our code, we just call DROPEFFECT_LINK, one
of the constants for drag and drop. I suggest you look them up
in the MFC docs.
When we move inside a registered D&D window, the D&D API will call
IDropTarget::DragOver(), and we call GotDrag(); My implementation
from the code does some checking for runtime classes and depending
on the window we are in sends some GUI feedback. Check out the
implementation in ECMaterialDropText.cpp. Note that Windows itself
shows the classical not valid drop cursor if the window the cursor
is over is not registered. We also return one of the constants to
give GUI feedback for the mouse cursor.
Last, if the user releases the mouse button in a registered window,
we get a “notification” call on IDropTarget::Drop(). The code there
checks if the data that the D&D API placed in the clipboard is OK
for us, and if so calls GotDrop(). We retrieve the text data, and
set it in our control.
The D&D API on exit of Drop() returns after the modal loop of
DoDragDrop(); and we free the clipboard data. Mission accomplished!
Remarks
Most of the code in the sample is really cut and paste, added
after we derived controls with the classwizard. All the mouse
button and move stuff is the same, you could put this in a generic
class and inherit from it (if you like multiple inheritance).
Dont forget the all important AfxOleInit() in your code. If you
dont add that one the Register function will fail.
Error feedback for the code could be better. Hey, I wrote this
sample up in an hour or two…
The sample
The sample shows all I wrote down above. As they say, one code
snippet shows more then 100000 lines of documentation. If it seems
overwhelming at first, just dig in. For reading on the COM stuff,
I would suggest a copy of Don Box excellent COM book.
If you have questions, gimme a mail at
erik.vangrunderbeeck@village.uunet.be. But do keep in mind
that to answer my mail on a regular base, I have to dig through
300 emails a day, and that means 2 weeks between receipt and reply
(unless you send mail like “I would like you to have my son…”).
Nah. My wife wouldnt like that 😉
Have fun and write some great code