This article gives an introduction on using clipboard and drag & operations containing useful tips and extensions not found in other sources. The main focus of this post is drop descriptions.
Introduction
This article describes how to add drag & drop support to MFC applications. It will start with an introduction on using clipboard and drag & operations containing useful tips and extensions not found in other sources. The main focus is on drop descriptions. Drop descriptions can be used to show the Aero drag cursors with optional text that has been introduced with Windows Vista. Because drop descriptions require that an application supports drag images, this is also explained in detail.
The topics handled here are:
All code in this article uses the MFC OLE classes that encapsulate the low level OLE data interfaces. To implement the required functions, we will derive our own classes from these MFC classes. The code snippets shown here in the article are based on the sources but have been shortened by removing comments, error checking, and unnecessary information.
To use the MFC OLE classes, the initialisation function AfxOleInit()
must be called from InitInstance()
and the header file afxole.h must be included (best place is the stdafx.h file). Because forgetting to initialise OLE is a common error resulting in unexpected behaviour, I will shout a little bit:
NOTE: Don't forget to call AfxOleInit()
.
The COleDataSource
class contains all necessary functions to put data on the clipboard and start a drag & drop operation. The preparation of data is identical for both methods. So it is a good idea to provide some data preparation functions for common clipboard formats.
After deriving our class COleDataSourceEx
from COleDataSource
we can add a function to prepare plain text data:
bool COleDataSourceEx::CacheString(CLIPFORMAT cfFormat, LPCTSTR lpszText)
{
HGLOBAL hGlobal = NULL;
size_t nSize = (_tcslen(lpszText) + 1) * sizeof(TCHAR);
if (nSize > sizeof(TCHAR)) {
hGlobal = ::GlobalAlloc(GMEM_MOVEABLE, nSize);
if (NULL != hGlobal)
{
LPVOID lpBuf = ::GlobalLock(hGlobal);
::CopyMemory(lpBuf, lpszText, nSize);
::GlobalUnlock(hGlobal);
CacheGlobalData(cfFormat, hGlobal);
}
}
return NULL != hGlobal;
}
This function allocates a global memory object, copies the string
including the terminating NULL
character to it, and finally caches the data to be set to the clipboard or used as drag source later. Note that the global memory object must be allocated using GMEM_MOVEABLE
or GHND
when using it as data source. To access such allocated data, the GlobalLock()
function must be used and the memory must be unlocked when finished using it.
We can now use our class and this function from any window class to copy a text string to the clipboard:
void CMyWnd::OnCopy()
{
CString strText = GetSelectedText();
if (!strText.IsEmpty())
{
COleDataSourceEx * pDataSrc = new COleDataSrcEx;
#ifdef _UNICODE
pDataSrc->CacheString(CF_UNICODETEXT, strText.GetString());
#else
pDataSrc->CacheString(CF_TEXT, strText.GetString());
#endif
pDataSrc->SetClipboard();
pDataSrc->FlushClipboard();
}
}
That's all and it is quite simple. But there is a pitfall that is not clearly stated in the Microsoft documentation:
NOTE: The data source object must be allocated on the heap using new
and must not be deleted or released after calling FlushClipboard()
!
The reason is that the FlushClipboard()
function calls ::OleFlushClipboard()
which calls Release()
for the encapsulated IDataObject
which finally results in deletion of the object. When calling COleDataSource::InternalRelease
instead of flushing the clipboard, the object will be deleted too but the data will be no longer on the clipboard. When calling neither, the object will be deleted when new data is put on the clipboard anywhere.
When providing CF_TEXT
or CF_OEMTEXT
, the code page of the text must be known for conversion when the receiving application uses Unicode or another code page. To specify the encoding, an application putting ANSI or OEM (MS-DOS) text on the clipboard or providing such text via drag & drop should also provide a CF_LOCALE
object containing the LCID
used when creating the text. So we can add another function to cache the LCID
:
bool COleDataSourceEx::CacheLocale(LCID nLCID )
{
HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(LCID));
if (hGlobal)
{
LCID *lpLCID = static_cast<LCID *>(::GlobalLock(hGlobal));
*lpLCID = (0 == LANGIDFROMLCID(nLCID)) ?
::GetThreadLocale() : ::ConvertDefaultLocale(nLCID);
::GlobalUnlock(hGlobal);
CacheGlobalData(CF_LOCALE, hGlobal);
}
return NULL != hGlobal;
}
When passing zero for the LCID
, the locale of the thread is used. ConvertDefaultLocale()
returns the effective LCID
when passing LOCALE_USER_DEFAULT
or LOCALE_SYSTEM_DEFAULT
.
NOTE: When providing CF_TEXT
or CF_OEMTEXT
data, also provide the CF_LOCALE
format.
Now let us use the same data for drag & drop passing also the LCID
. This is usually started when pressing the left mouse button. So we have to handle the WM_LBUTTONDOWN
message in our window class. There are two controls that already have a start drag detection: List controls and tree view controls. With these controls, handle the LVN_BEGINDRAG
respectively the TVN_BEGINDRAG
message.
void CMyWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
bool bHandled = false; bool bSimulateClick = false; if ((GetFocus() == this || ShowSelAlways()) && OnSelection(point))
{
CString strText = GetSelectedText();
if (!strText.IsEmpty())
{
COleDataSourceEx * pDataSrc = new COleDataSrcEx;
#ifdef _UNICODE
pDataSrc->CacheString(CF_UNICODETEXT, strText.GetString());
pDataSrc->CacheMultiByte(CF_TEXT, strText.GetString());
#else
pDataSrc->CacheUnicode(CF_UNICODETEXT, strText.GetString());
pDataSrc->CacheString(CF_TEXT, strText.GetString());
#endif
pDataSrc->CacheLocale();
DROPEFFECT dwEffect = DROPEFFECT_COPY;
if (IsWindowEnabled() && !IsReadOnly())
dwEffect |= DROPEFFECT_MOVE;
if (DROPEFFECT_MOVE == pDataSrc->DoDragDropEx(dwEffect, NULL))
Clear();
if (DRAG_RES_RELEASED == pDataSrc->GetDragResult())
bSimulateClick = true;
pDataSrc->InternalRelease();
bHandled = true;
}
}
if (!bHandled || bSimulateClick)
{
CMyWnd::OnLButtonDown(nFlags, point); if (bSimulateClick) SendMessage(WM_LBUTTONUP, nFlags, MAKELONG(point.x, point.y));
}
}
Care must be taken when to start dragging and when not. Some general rules are:
- There must be something selected in the control or the complete content is dragged by default.
- When something must be selected, the click must occur on the selection.
- When something must be selected and the selection is not shown when the control does not have the focus, the control must have the focus.
The last point applies to most controls. They will, by default, not show the current selection when they did not have the focus. For edit controls, this behaviour may be changed by setting the ES_NOHIDESEL
style before creating the control.
Note that data is provided as CF_TEXT
and CF_UNICODETEXT
. With clipboard operations, the system returns converted data if the requested text format is not present but any other (ANSI, Unicode, or OEM/DOS). But this does not apply to drag & drop operations. So we should provide all possible formats (CF_OEMTEXT
is excluded here).
NOTE: With drag & drop operations, there is no implicit conversion for standard clipboard formats when retrieving data.
You may have noted the InternalRelease()
function call. This is required here to delete the object. While it is possible to use delete
or create the object on the stack with Drag & Drop operations, it is not recommended to do so.
NOTE: When starting drag & drop operations, the COleDataSource
object is not destroyed automatically.
Note also that the default button down handler is not called when a drag operation has been started (even when it has been cancelled). This is necessary to ensure that the internal mouse button state of the control window is in a defined state. When calling DoDragDrop()
, all following mouse events (especially the final button up event) are captured until the function returns. But there is a special case: When the drag operation has not been started. A drag operation is finally started when the mouse leaves a defined area (1 pixel wide rectangle by default) or a specific time elapsed (200 ms by default). If a drag operation has not been started by releasing the mouse button immediately, this single click may be passed to the control by calling the default handler and sending a button up message afterwards. Because the COleDataSource
class does not provide a method to detect this, it has been implemented in the COleDataSourceEx
class when calling DoDragDropEx()
. See the CMyEdit::OnLButtonDown()
source for an example.
NOTE: Don't call the default mouse button down handler when a drag operation has been started.
As noted above, there is a delay time before the drag operation is started. If the drag operation should start immediately without waiting, pass a pointer to a null
rectangle (that is a rectangle with all members set to zero). This is not mentioned in the MSDN.
To access data from the clipboard, use the COleDataObject
class and attach the clipboard. Here is a simple example for a CEdit
derived class. This is just an example; real applications would just use CEdit::Paste()
.
void CMyEdit::OnPaste()
{
COleDataObject Data;
Data.AttachClipboard();
#ifdef _UNICODE
HGLOBAL hGlobal = Data.GetGlobalData(CF_UNICODETEXT);
#else
HGLOBAL hGlobal = Data.GetGlobalData(CF_TEXT);
#endif
if (hGlobal)
{
LPCTSTR lpszText = static_cast<LPCTSTR>(::GlobalLock(hGlobal));
ReplaceSel(lpszText, 1);
::GlobalUnlock(hGlobal);
::GlobalFree(hGlobal);
}
}
Note the final GlobalFree()
call to delete the memory. The global memory handle returned by COleDataObject::GetGlobalData()
is always a new memory block that has been allocated and filled with a copy of the memory passed on the source side. When using COleDataObject::GetData()
, call ReleaseStgMedium()
afterwards to release the data. This is not clearly stated in the MSDN documentation.
NOTE: Don't forget to free global memory and release medium when retrieving data.
The standard text formats CF_TEXT
, CF_OEMTEXT
, and CF_UNICODETEXT
must contain null
terminated string
s. But this does not apply to other text formats like CSV, and RTF. And there may be applications that did not copy the null
terminator even with the standard text formats. So it might be useful to check this. The GlobalSize()
function can be used to get the size of the allocated memory and limit the length if the string
is not null
terminated. See the COleDropTargetEx::GetString()
function in the sources. It will copy text data into allocated memory with size checking and performs Unicode / code page conversions if necessary using CF_LOCALE
if present.
NOTE: Don't rely on null terminated strings when getting text data from the clipboard or by drag & drop.
MFC provides the COleDropTarget
class to allow windows to accept drop commands. But this class supports CView
derived classes only. So we must implement drop support for other classes by using our own COleDropTarget
derived class. Because support for drag images will be added later to this class, it should be also used for CView
based classes.
To register a window as a drop target, add a COleDropTarget
member to the window class and call the Register()
function passing the CWnd
pointer to the window. The common place to do this is OnInitialUpdate()
with view windows, OnInitDialog()
with dialog windows and OnCreate()
with all other windows.
But with template created windows (e.g., controls created when loading a dialog template), OnCreate()
is not called (the window is created before the CWnd
wrappers so that CWnd::OnCreate()
is never called). For such controls, registering can be performed using the virtual CWnd
function PreSubclassWindow()
or from OnInitDialog()
of the parent dialog. In that case, the control window should provide an initialisation function that registers the window and optionally performs other tasks. An example would be the initialisation function for a CEdit
derived class that can also set the initial content and the text limit:
void CMyEdit::Init(LPCTSTR lpszText , unsigned nLimitSize )
{
if (NULL == m_pDropTarget)
m_DropTarget.Register(this);
if (nLimitSize)
SetLimitText(nLimitSize);
if (lpszText)
SetWindowText(lpszText);
}
The window will be registered as drop target when the CWnd
member variable m_pDropTarget
is NULL
. This variable is set to the address of the COleDropTarget
when registering. It is used by CWnd::OnNcDestroy()
to revoke (unregister) the window. Checking for NULL
here is necessary to avoid an assertion in debug builds when calling the initialisation function again.
NOTE: With template created windows, CWnd::OnCreate()
is not called.
Drop Support for Non CView Based Windows
The problem is that the drop event handlers of the COleDropTarget
class must call corresponding functions of the window class. COleDropTarget
solves this by checking if the passed CWnd*
is of the type CView
. If the check is successful, the CWnd*
is casted to CView*
and the corresponding virtual handler function is called (here for OnDragEnter
):
if (!pWnd->IsKindOf(RUNTIME_CLASS(CView)))
return DROPEFFECT_NONE;
CView* pView = (CView*)pWnd;
ASSERT_VALID(pView);
return pView->OnDragEnter(pDataObject, dwKeyState, point);
This technique may be also used for other windows. But it requires including of header files and adding similar code for all classes that must be supported. I don't like this solution for two reasons: The drop target class depends on the supported window classes so that it is project specific and it uses IsKindOf()
which should be avoided (at least in release code).
Alternative options are the usage of callback functions or sending user defined messages. I decided to implement both methods:
DROPEFFECT COleDropTargetEx::OnDragEnter
(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
DROPEFFECT dwRet = DROPEFFECT_NONE;
if (m_pOnDragEnter) dwRet = m_pOnDragEnter(pWnd, pDataObject, dwKeyState, point);
else if (m_bUseMsg) {
DragParameters Params = { 0 };
Params.dwKeyState = dwKeyState;
Params.point = point;
dwRet = static_cast<DROPEFFECT>(pWnd->SendMessage(
WM_APP_DRAG_ENTER,
reinterpret_cast<WPARAM>(pDataObject),
reinterpret_cast<LPARAM>(&Params)));
}
else dwRet = COleDropTarget::OnDragEnter(pWnd, pDataObject, dwKeyState, point);
return dwRet;
}
See the COleDropTargetEx
sources for the other handlers.
When using callback functions, the target window must pass them before registering. Because callback functions must be static, the target window classes must cast the passed CWnd*
parameter to their class and use this pointer to access members or call a non-static version of the function. The CMyListCtrl
class from the demo application uses this method.
The member variable m_bUseMsg
specifies if the drop target window handles user defined messages for the drop events. It must be set by the target window before registering. Because most drag events have more than two parameters, we must pass them using the DragParameters
structure. This structure and the WM_APP
based message codes are defined in COleDropTargetEx.h. The CMyEdit
class from the demo application uses this method.
If neither sending messages nor using callback functions, the default handler of the base class is called. This will call the virtual handler functions of CView
based classes.
Not all of the handler functions must implemented. For basic drop support, add OnDragEnter()
, OnDragOver()
, and OnDropEx()
or OnDrop()
.
OnDragEnter
This is called when dragging the first time (or again after leaving) over the window. At first, it should be checked if the window is able to drop data (window is enabled and an optional read only state is not set). Then check if the drag source provides data that can be dropped. Additional checks may be if the provided data would fit (e.g., number of text characters is less than a specified limit). The result of these checks may be stored in a member variable to be used by OnDragOver()
. The returned drop effect should be determined like the one returned by OnDragOver()
.
OnDragOver
The main purpose is to set the drop effect according to the key state (e.g., copying when the Control key is down and moving otherwise), and to check if data can be dropped on the current position (e.g., over the client area and not over a scroll bar or the header of a list control). This is called repeatedly when moving around the window (like the WM_MOUSEMOVE
message) and when the key state (Shift, Ctrl, Alt) changes. So don't perform time consuming tasks inside this function. This is also called before OnDropEx()
and OnDrop()
to get the drop effect to be passed to these functions. Because the returned drop effect is used to determine the type of cursor and returned to the source upon dropping, it should be a single effect and not a combination of multiple effects.
OnDragLeave
This is called when leaving the window. Use it for clean-up. With most windows, there is no need to implement this handler. If it is present to perform some clean-up, it may be necessary to add similar code to the drop handlers because OnDragLeave()
is not called when a dropping occurs.
OnDrop and OnDropEx
Only one of these functions must be implemented. They are called when releasing the mouse button when over the window. Get the data here and insert them into the control when dropping should occur. When the mouse button is released, these functions are called:
OnDragOver()
is called to get the drop effect to be passed to OnDropEx()
and/or OnDrop()
. OnDropEx()
is called (even when the drop effect is DROPEFFECT_NONE
). - When
OnDropEx()
returns -1
and the drop effect is not DROPEFFECT_NONE
, OnDrop()
is called. - When
OnDropEx()
returns -1
and the drop effect is DROPEFFECT_NONE
, OnDragLeave()
is called.
The drop effect finally passed to the DoDragDrop()
function of the source is:
- The value returned by
OnDropEx()
if not -1
, - else the value returned by
OnDragOver()
if OnDrop()
returns TRUE
, - else
DROPEFFECT_NONE
.
Because OnDropEx()
is always called even when the drop effect is DROPEFFECT_NONE
, OnDropEx()
should check the passed value and return without dropping in this case.
OnDragScroll
This can be handled to perform auto scrolling when dragging over a scrollbar or an inset region (a small band inside the borders of the client area). OnDragScroll()
is the first handler called when entering or moving over a window. When scrolling is active, the DROPEFFECT_SCROLL
bit set is set in the return value to avoid further processing. If the scroll bit is not set, OnDragEnter()
respectively OnDragOver()
is called.
COleDropTarget
supports scrolling for inset regions of view windows. The COleDropTargetEx
class from the sources provides a default handling that calls a callback function or sends a user defined message when scrolling should be performed. The default handling is enabled with a single call of a configuration function passing flags for the scroll source (horizontal bar, vertical bar, and inset region) and the call back function. The CMyEdit
and CMyListCtrl
classes from the demo program use this option.
Drop Handlers as Callback Functions
When using callback functions, add the static
functions to the window classes and pass their addresses to the drop target class before registering:
class CMyWnd : public CWnd
{
public:
void Init();
protected:
static DROPEFFECT CALLBACK OnDragEnter
(CWnd *pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);
virtual DROPEFFECT OnDragEnter
(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point);
};
void CMyWnd::Init()
{
if (NULL == m_pDropTarget)
{
m_DropTarget.SetOnDragEnter(OnDragEnter);
m_DropTarget.Register(this);
}
}
DROPEFFECT CMyWnd::OnDragEnter
(CWnd *pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
ASSERT(pWnd->IsKindOf(RUNTIME_CLASS(CMyWnd)));
CMyWnd *pThis = static_cast<CMyWnd*>(pWnd);
return pThis->OnDragEnter(pDataObject, dwKeyState, point);
}
Drop Handlers for User Defined Messages
When using user defined messages, add standard message handlers returning a LRESULT
and accepting a WPARAM
and LPARAM
parameter, and enable the usage of messages before registering:
class CMyWnd2 : public CWnd
{
protected:
virtual void PreSubclassWindow();
LRESULT OnDropEx(WPARAM wParam, LPARAM lParam);
};
ON_MESSAGE(WM_APP_DROP_EX, OnDropEx)
void CMyWnd2::PreSubclassWindow()
{
m_DropTarget.SetUseMsg();
m_DropTarget.Register(this);
CWnd::PreSubclassWindow();
}
LRESULT CMyWnd2::OnDropEx(WPARAM wParam, LPARAM lParam)
{
COleDataObject *pDataObject = reinterpret_cast<COleDataObject*>(wParam);
COleDropTargetEx::DragParameters *pParams =
reinterpret_cast<COleDropTargetEx::DragParameters*>(lParam);
DROPEEFECT dwEffect = (DROPEFFECT)-1;
if (pParams->dropEffect)
{
}
return dwEffect;
}
The message handlers must cast the WPARAM
parameter to COleDataObject*
and the LPARAM
parameter to DragParameters*
.
NOTE: When using this method, a WM_APP_DROP_EX
handler must be always present. If it does not handle the event, it must return -1
to indicate this.
Drop Handlers for CView Based Windows
With CView
based classes, just override the virtual drag event functions. However, the other methods may be also used.
With Windows 2000, drag images has been introduced allowing the display of alpha-blended images during drag & drop operations. Such images must be provided by the drag source and drop targets are responsible for displaying them.
On the source side, we use the IDragSourceHelper
interface by adding a member variable to our COleDataSourceEx
class and initialise it in the constructor:
class COleDataSourceEx : public COleDataSource
{
protected:
IDragSourceHelper * m_pDragSourceHelper;
};
COleDataSourceEx::COleDataSourceEx()
{
m_pDragSourceHelper = NULL;
::CoCreateInstance(CLSID_DragDropHelper,
NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_pDragSourceHelper));
}
COleDataSourceEx::~COleDataSourceEx()
{
if (NULL != m_pDragSourceHelper)
m_pDragSourceHelper->Release();
}
IDragSourceHelper
provides two functions to specify the drag image. So we add corresponding public
functions to our class:
bool COleDataSourceEx::SetDragImageWindow(HWND hWnd, POINT* pPoint)
{
HRESULT hr = E_NOINTERFACE;
if (m_pDragSourceHelper)
{
hr = m_pDragSourceHelper->InitializeFromWindow(hWnd, pPoint,
static_cast<LPDATAOBJECT>(GetInterface(&IID_IDataObject)));
}
return SUCCEEDED(hr);
}
When using this function, the control window which HWND
is passed is responsible for creating the drag image. It must respond to the registered DI_GETDRAGIMAGE
message and fill the passed SHDRAGIMAGE
structure. List controls and tree view controls already contain such a message handler so that no further action is required with these controls. But with list controls in report mode, only the first column is copied to the image while the image width is prepared to hold all columns. It is possible to pass NULL
for the HWND
parameter. In this case, a generic image will be generated by the system. If a global data object "Shell IDList Array" exists, this is used to determine the drag image (like when dragging files from the Explorer).
If the control window does not handle the DI_GETDRAGIMAGE
message, the drag image can be specified by passing it as bitmap
:
bool COleDataSourceEx::SetDragImage(HBITMAP hBitmap, const CPoint* pPoint, COLORREF clr)
{
HRESULT hr = E_NOINTERFACE;
if (hBitmap && m_pDragSourceHelper)
{
BITMAP bm;
SHDRAGIMAGE di;
VERIFY(::GetObject(hBitmap, sizeof(BITMAP), &bm));
di.sizeDragImage.cx = bm.bmWidth;
di.sizeDragImage.cy = bm.bmHeight;
if (pPoint)
di.ptOffset = *pPoint;
else
{
di.ptOffset.x = di.sizeDragImage.cx >> 1;
di.ptOffset.y = di.sizeDragImage.cy >> 1;
}
di.hbmpDragImage = hBitmap;
di.crColorKey = (CLR_INVALID == clr) ? ::GetSysColor(COLOR_WINDOW) : clr;
hr = m_pDragSourceHelper->InitializeFromBitmap(&di,
static_cast<LPDATAOBJECT>(GetInterface(&IID_IDataObject)));
}
if (FAILED(hr) && hBitmap)
::DeleteObject(hBitmap); return SUCCEEDED(hr);
}
This function passes a bitmap as drag image. pPoint
specifies the position of the cursor relative to the top left corner of the image, and clr
specifies the transparent colour of the image. The drag helper will own the bitmap and release it when dragging has been finished.
At this point, I should explain how the transparency is achieved: Each pixel of the transparent colour in the image is changed to black, and the corresponding bits are used to generate a mask. Therefore, the drag image should not contain black pixels if the transparent colour is not also black. Otherwise, those pixels will be not visible.
NOTE: Black pixels in drag images are not visible.
When now executing these functions, the calls to the IDragSourceHelper
functions will fail with DATA_E_FORMATETC
("Invalid FORMATETC structure"). The reason is that the COleDataSource::XDataObj::SetData()
function tries to find existing entries in a second (set) cache. But that cache is empty by default and adding data to it must be implemented. But even that would not help because for clipboard and Drag & Drop operations we have to use the first (get) cache instead. So we have to override this function and cache new data or update existing. Because the IDataObject
interface functions are pure virtual, we must add all of them and not only the SetData()
function.
See the article DragSourceHelper MFC by Carsten Leue for more information. The sources are based on that article.
Now that drag images can be passed, how do we create them? Depending on the type of the source control, there are many options. The simplest solution (from the code point of view) is loading them from the resources:
bool COleDataSourceEx::InitDragImage(int nResBM, const CPoint* pPoint, COLORREF clr)
{
bool bRet = false;
if (m_pDragSourceHelper)
{
HBITMAP hBitmap = ::LoadBitmap(AfxGetResourceHandle(), MAKEINTRESOURCE(nResBM));
if (hBitmap)
bRet = SetDragImage(hBitmap, pPoint, clr);
}
return bRet;
}
To get icons for specific file types, use the SHGetFileInfo()
function. It will fill in a SHFILEINFO
structure with the icon that matches the specified file name.
The COleDataSourceEx
class source for this article provides some functions to create drag images:
- From a
CBitmap
- From a
bitmap
loaded from the resources - From a file type icon retrieved by specifying the file name extension
- From a text string using the font and size of a passed
CWnd
- By capturing a region from a passed
CWnd
with optional scaling - By copying a
bitmap
with optional scaling (useful when dragging a bitmap)
Here are some examples from the demo application when dragging from the list control (captured from window, generated from text using transparent grey text and system selection colours):
As already noted above, the images should not contain black pixels when the transparent colour is not black. But there may be image sources that contain black pixels. To handle these, the COleDataSourceEx
class provides a function to replace black pixels of high colour bitmaps by the closest colour.
You may have seen drag images that are ghosted (the outer areas of the image are blended out) and images that are shown as they are. This ghosting is performed by the system when the width or height of the drag image exceeds 300 pixels.
To show drag images when over a window, the window must call the corresponding drag event handler functions of the IDropTargetHelper
interface. To receive the drag events, such a window must register itself as a drop target using the COleDropTarget
class. So we can simply add an IDropTargetHelper
member to our COleDropTargetEx
class, initialise it in the constructor, and call the corresponding handler functions:
class COleDropTargetEx : public COleDropTarget
{
protected:
IDropTargetHelper* m_pDropTargetHelper; };
COleDropTargetEx::COleDropTargetEx()
{
m_pDropTargetHelper = NULL;
::CoCreateInstance(CLSID_DragDropHelper,
NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_pDropTargetHelper));
}
COleDropTargetEx::~COleDropTargetEx()
{
if (NULL != m_pDropTargetHelper)
m_pDropTargetHelper->Release();
}
DROPEFFECT COleDropTargetEx::OnDragEnter
(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
DROPEFFECT dwRet = COleDropTarget::OnDragEnter
(pWnd, pDataObject, dwKeyState, point);
if (m_pDropTargetHelper)
m_pDropTargetHelper->DragEnter(pWnd->GetSafeHwnd(),
m_lpDataObject, &point, dwRet);
return dwRet;
}
Please see the sources for this article for the other event handlers. m_lpDataObject
is a COleDropTarget
member pointing to the OLE data object interface. The COleDataObject
parameter is attached to this object. Because we have to pass it to the drop target helper, we can just use the member variable rather than getting it from the parameter using GetIDataObject()
.
That's all. Now each window using our COleDropTargetEx
class and registering itself as a drop target will show drag images when dragging over them.
But what about the windows that did not handle drag events?
To show drag images when over any window of an application, add a COleDropTargetEx
member to the main window of the application and register it as a drop target. Then this window will handle the events and show the drag image when no client window does this. With dialog applications, this window is the main dialog and registering should be done in OnInitDialog()
. With main frame based applications, do this for the main frame window class and register from within OnCreate()
. There is no need to add drag event handlers when the main window should not support dropping.
TIP: To show drag images when over any window of an application, add the required handlers to the main window of the application.
With Windows Vista, drop descriptions has been introduced providing new Aero cursors with optional text. An example is the Windows Explorer showing the new cursors with text when dragging files. But this feature is rarely documented. Therefore, it will be explained in detail here.
By default, no text is shown when using the new cursors. To enable text, a new interface has been inherited from IDragSourceHelper
providing one more function to enable displaying text with the drag cursor. So we should first update our COleDataSourceEx
constructor:
class COleDataSourceEx : public COleDataSource
{
protected:
IDragSourceHelper * m_pDragSourceHelper;
IDragSourceHelper2 * m_pDragSourceHelper2;
};
COleDataSourceEx::COleDataSourceEx()
{
m_pDragSourceHelper = NULL;
m_pDragSourceHelper2 = NULL;
::CoCreateInstance(CLSID_DragDropHelper,
NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_pDragSourceHelper));
if (m_pDragSourceHelper)
{
m_pDragSourceHelper->QueryInterface(IID_PPV_ARGS(&m_pDragSourceHelper2));
if (m_pDragSourceHelper2)
{
m_pDragSourceHelper->Release();
m_pDragSourceHelper = static_cast<IDragSourceHelper*>(m_pDragSourceHelper2);
}
}
}
When the drag source helper has been created, the code tries to access the IDragSourceHelper2
interface. If this is successful, we can use the new cursors (the member variable m_pDragSourceHelper2
can be now also used to indicate that the application is running on Vista or later). Because the new interface is inherited from the old one, the pointer of the new interface can be assigned after releasing the old one.
We can now access the new function to enable text:
bool COleDataSourceEx::AllowDropDescriptionText()
{
return m_pDragSourceHelper2 ?
SUCCEEDED(m_pDragSourceHelper2->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT)) : false;
}
This function must be called before InitializeFromWindow()
or InitializeFromBitmap()
. Otherwise, text would not be shown.
To show the new cursors, the drop target window must show the drag image and the drag image window itself must be invalidated with each mouse move while dragging. This invalidation should be performed by the drag source.
To get a notification with each mouse movement during a drag operation, we will derive a class from COleDropSource
that implements the virtual GiveFeedback()
function. This function is called whenever a drop target handler returns. A local instance of this derived class is then passed to the COleDropSource::DoDragDrop()
function:
DROPEFFECT COleDataSourceEx::DoDragDropEx
(DROPEFFECT dwEffect, LPCRECT lpRectStartDrag )
{
COleDropSourceEx dropSource;
bool bUseDescription = (m_pDragSourceHelper2 != NULL) && ::IsAppThemed();
dropSource.m_pIDataObj = bUseDescription ?
static_cast<LPDATAOBJECT>(GetInterface(&IID_IDataObject)) : NULL;
return DoDragDrop(dwEffect, lpRectStartDrag, static_cast<COleDropSource*>(&dropSource));
}
When drop descriptions can be used, a pointer to the OLE data object interface is passed to access the cached data (the COleDataSource
class provides only functions to write data but we must also read data objects). Drop descriptions can be used with Vista or later (IDragSourceHelper2
interface is present) and enabled visual styles. IsAppThemed()
requires linking with uxtheme.lib (with COleDataSourceEx
this is done using a pragma statement). When the application must support Windows versions prior to XP, the uxtheme.dll must be delay loaded by adding it to the appropriate project linker setting.
Now let's have a look on our COleDropSourceEx
class:
class COleDropSourceEx : public COleDropSource
{
public:
COleDropSourceEx();
protected:
bool m_bSetCursor; LPDATAOBJECT m_pIDataObj;
bool SetDragImageCursor(DROPEFFECT dwEffect);
virtual SCODE GiveFeedback(DROPEFFECT dropEffect);
public:
friend class COleDataSourceEx;
};
COleDropSourceEx::COleDropSourceEx()
{
m_bSetCursor = true; m_pIDataObj = NULL; }
The function that updates the drag image cursor is the GiveFeedback()
function:
SCODE COleDropSourceEx::GiveFeedback(DROPEFFECT dropEffect)
{
SCODE sc = COleDropSource::GiveFeedback(dropEffect);
if (m_bDragStarted && m_pIDataObj)
{
if (0 != CDragDropHelper::GetGlobalDataDWord(m_pIDataObj, _T("IsShowingLayered")))
{
if (m_bSetCursor)
{
HCURSOR hCursor = (HCURSOR)::LoadImage(
NULL, MAKEINTRESOURCE(OCR_NORMAL), IMAGE_CURSOR, 0, 0,
LR_DEFAULTSIZE | LR_SHARED);
::SetCursor(hCursor);
m_bSetCursor = false;
}
SetDragImageCursor(dropEffect); sc = S_OK; }
else
m_bSetCursor = true;
}
return sc;
}
After calling the base class function, we check if drop descriptions can be used. This requires that dragging has been started and COleDataSourceEx
has initialised the interface member variable. The next step is reading the boolean "IsShowingLayered
" data object. CDragDropHelper::GetGlobalDataDWord()
is a helper function that reads a DWORD
value stored in a global data object using IDataObject::GetData()
. "IsShowingLayered
" is set to true
by the IDropTargetHelper
when the drop target is showing the drag image. If this is false
, the old style drag cursor will be shown. Otherwise, the default new style cursor for the corresponding drop effect is shown by calling SetDragImageCursor(dropEffect)
. But before doing this, we must set the Windows cursor to the default arrow. This is necessary when entering a drop target that shows the drag image and the previous target hasn't shown it. When not doing so, the old style drag cursor would be still visible. Finally, we have to change the return value to SC_OK
to indicate that the cursor has been updated and we don't want the old style cursors.
Now to the SetDragImageCursor()
function:
bool COleDropSourceEx::SetDragImageCursor(DROPEFFECT dwEffect)
{
HWND hWnd = (HWND)ULongToHandle(GetGlobalDataDWord(_T("DragWindow")));
if (hWnd)
{
WPARAM wParam = 0; switch (dwEffect & ~DROPEFFECT_SCROLL)
{
case DROPEFFECT_NONE : wParam = 1; break;
case DROPEFFECT_COPY : wParam = 3; break;
case DROPEFFECT_MOVE : wParam = 2; break;
case DROPEFFECT_LINK : wParam = 4; break;
}
::SendMessage(hWnd, WM_USER + 2, wParam, 0);
}
return NULL != hWnd;
}
The global data object "DragWindow
" contains the handle of the drag image window stored in a DWORD
. To update the window, specific messages must be send. SetDragImageCursor()
uses an undocumented message to specify the cursor type using the WPARAM
parameter:
WPARAM | Description |
0 | Global data object "DropDescription " defines cursor type and optional text |
1 | Use stop sign cursor without text (can't drop) |
2 | Use arrow cursor with system default text (move) |
3 | Use plus sign cursor with system default text (copy) |
4 | Use curved arrow cursor with system default text (link) |
If WPARAM
is zero or a global "DropDescription
" data object exists and its image type member is valid, that is used to define the cursor type and the optional text. Otherwise, WPARAM
specifies the cursor type and the optional text is the system default text (like those shown when dragging files from the Windows Explorer). There is another documented message with message code DDWM_UPDATEWINDOW
(WM_USER+3
) and WPARAM
and LPARAM
both set to zero. This will use the cursor and text from the global data object "DropDescription
" similar to the WM_USER+2
message with WPARAM
zero.
The above examples use the default drag cursors with optional text. But cursor type and text may be also specified by the drag source and the drop target. To implement this, the global "DropDescription
" data object must be created or changed if it exists already. This data object uses the well documented DROPDESCRIPTION
structure. From within our COleDropSourceEx::GiveFeedback()
function, we must check if the description has been changed by the drop target. If not, we may optionally change it on the source side. If the description is present and valid, it is used by the drag image window. Otherwise, the default cursor and text is shown according to the drop effect:
SCODE COleDropSourceEx::GiveFeedback(DROPEFFECT dropEffect)
{
SCODE sc = COleDropSource::GiveFeedback(dropEffect);
if (m_bDragStarted && m_pIDataObj)
{
bool bOldStyle =
(0 == CDragDropHelper::GetGlobalDataDWord(m_pIDataObj, _T("IsShowingLayered")));
if ((bOldStyle && !m_bSetCursor) || (m_pDropDescription && !bOldStyle))
{
FORMATETC FormatEtc;
STGMEDIUM StgMedium;
if (CDragDropHelper::GetGlobalData(m_pIDataObj, CFSTR_DROPDESCRIPTION,
FormatEtc, StgMedium))
{
bool bChangeDescription = false; DROPDESCRIPTION *pDropDescription =
static_cast<DROPDESCRIPTION*>(::GlobalLock(StgMedium.hGlobal));
if (bOldStyle)
bChangeDescription = CDragDropHelper::ClearDescription(pDropDescription);
else if (pDropDescription->type <= DROPIMAGE_LINK)
{
DROPIMAGETYPE nImageType = CDragDropHelper::DropEffectToDropImage(dropEffect);
if (DROPIMAGE_INVALID != nImageType &&
pDropDescription->type != nImageType)
{
if (m_pDropDescription->HasText(nImageType))
{
bChangeDescription = true;
pDropDescription->type = nImageType;
m_pDropDescription->CopyText(pDropDescription, nImageType);
}
else
{
bChangeDescription =
CDragDropHelper::ClearDescription(pDropDescription);
}
}
}
::GlobalUnlock(StgMedium.hGlobal);
if (bChangeDescription) { if (FAILED(m_pIDataObj->SetData(&FormatEtc, &StgMedium, TRUE)))
bChangeDescription = false;
}
if (!bChangeDescription) ::ReleaseStgMedium(&StgMedium);
}
}
if (!bOldStyle) {
if (m_bSetCursor)
{
HCURSOR hCursor = (HCURSOR)LoadImage(
NULL,
MAKEINTRESOURCE(OCR_NORMAL),
IMAGE_CURSOR,
0, 0,
LR_DEFAULTSIZE | LR_SHARED);
::SetCursor(hCursor);
}
SetDragImageCursor(dropEffect);
sc = S_OK; }
m_bSetCursor = bOldStyle;
}
return sc;
}
In addition to the previous version of the feedback function, we will now get the DropDescription
and check if it must be updated. If it exists and the drop target does not show the drag image, we will clear the description. So we can detect if another drop target has changed it. If the description is present and the drop target shows the drag image, we must detect if the description has been changed by the drop target. If the image type is invalid (DROPIMAGE_INVALID
), we can be sure that the description has not been set by the target. If the image type is one of those that isn't supported here (special types beyond the corresponding drop effects), we can be sure that the description has been changed by the drop target. In all other cases, we assume that the description has been changed by the drop target if the image type matches the drop effect.
When the description exists and has not been changed by the target, we may set it here when using of text is enabled. There is no need to change the description when text is disabled: The image type defines the cursor to be used (the drop effect corresponding type when it is DROPIMAGE_INVALID
). m_pDropDescription
is a pointer to a COleDataSourceEx
member. It is a helper class holding the drop description strings for the image types. The pointer will be set to NULL
when no text is defined.
When the description exists and the szMessage
member is an empty string
, the image type should be set to invalid to show the default cursor. If not doing so, there will be an empty text area below and right of the cursor which may look uncommon.
Because this feedback function is called with each mouse movement, the description is only read and updated when necessary. When the description is present and valid, it will be used to define the cursor type and optional text (the WPARAM
of the drag image window update message is ignored). Otherwise, the image is selected according to the drop effect.
Changing Drop Descriptions on the Target Side
To implement this, just set the "DropDescription
" data object from OnDragEnter()
and OnDragOver()
of the COleDropTargetEx
class and reset it using DROPIMAGE_INVALID
as image type upon OnDragLeave()
. To show user defined messages for some effects and default text for others, set the drop description to invalid for those effects that should use the default text. The COleDropTargetEx
class provides functions to set the "DropDescription
" data and clears the structure upon leaving. Setting the drop description on the target side will not only work with our COleDropSourceEx
class, but also with all other drag sources that handle drop descriptions in this way (like the Windows Explorer).
The Info
list from the example application uses the DROPIMAGE_LABEL
type and sets a user defined text. This will look like this (first is from the list control of the demo application and second is from dragging a text file from the Explorer:
More Things to Observe
The OnDragEnter()
and OnDragOver()
handlers will usually return a drop effect according to the actual key state (e.g., moving by default and copying if the control key is down). But this effect may be not supported by the source (e.g., when only copying is supported). So the returned effect must be checked and changed if necessary. The COleDropTarget
class does this just before returning from the OLE interface handlers. But when changing the drop description, the filtering must be applied before to avoid showing wrong cursors and text. Because the allowed drop effects passed with DoDragDrop()
from the source are not stored by the COleDropTarget
class, they must be retrieved and stored by the COleDropTargetEx
class. This requires implementation of interface mapping like with the COleDataSource
class.
When a drop description data object exists, it is always used by the system to show the new style cursor with optional text when over a window that supports drag images. This results in having old and new style cursors on the screen when the drag source provides a drag image but did not support new style cursors (does not change the Windows cursor within the GiveFeedback()
function). Therefore, a drop target should not create a drop description data object when it is not sure that new style cursors are supported by the source.
Data Objects Used With Drag Images and Drop Descriptions
When using drag images, there are many data objects containing specific information (use the demo application to see them when dropping on the Info list). Most of these data objects are undocumented. Information here is from the web and from own research.
Format Name | Type | Description |
ComputedImage | DWORD | ? (1, 2) |
DisableDragText | BOOL | ? (1, 2) |
DragContext | IStream | Used internally by the Windows drag/drop helpers. |
DragImageBits | SHDRAGIMAGE | Drag image bitmap . Behind the structure are the image bits. |
DragSourceHelperFlags | DWORD | Value passed to IDragSourceHelper2::SetFlags() . Can be used to avoid updating of drop description text when not present or false. (1) |
DragWindow | DWORD | The HWND of the drag image window stored as DWORD . |
DropDescription | DROPDESCRIPTION | Cursor type and text to be used for new style cursor. |
IsComputingImage | BOOL | ? (1, 2) |
IsShowingLayered | BOOL | Set when a drop target shows the drag image. |
IsShowingText | BOOL | Set when a drop target requests update of the new style cursor. (3) |
UsingDefaultDragImage | BOOL | Drag image is the default one (e.g. when passing a NULL hWnd to IDragSourceHelper::InitializeFromWindow() or the window did not response to the DI_GETDRAGIMAGE message). When setting this during an active drag session, the image will change from the user specified image to the default image. (1) |
- Data object is only present when set once. If not present, boolean and
DWORD
values should be treated as false / zero. - Used when dragging from Explorer.
IsShowingText
is set to false
when there is no need to update the new style cursor (e.g., when entering or leaving a drop target window or data has been dropped). This can be used from within the GiveFeedback()
function to suppress the update of the cursor but the cursor may be invisible for very short moments.
The sources support Unicode builds and non Unicode builds using ANSI (single byte) code pages. The demo application requires Windows XP or later (calls Theme API functions without checking the Windows version). The MFC OLE derived classes are designed for Windows 2000 and later. To compile the sources, Visual Studio 2005 or later is required.
The sources may contain some interesting functions not mentioned here. So I will give a short overview of the provided files, classes, and supported operations.
COleDataSourceEx.cpp, COleDataSourceEx.h
Class COleDataSourceEx
:
- Support for drag images and drop descriptions
- Functions to cache and render plain text, CSV, HTML, and RTF
- Functions to cache and render images as
CF_BITMAP
, CF_DIB
, and CF_DIBV5
with conversion of DDBs to DIBs - Functions to cache and render TIFF, JPEG, GIF, and PNG images
- Functions to create drag images from resource, bitmap, text, file type icon, and window regions
- Function to cache data as virtual files
- Supports user defined drop description text
COleDropSourceEx.cpp, COleDropSourceEx.h
Class COleDropSourceEx
:
- Support for drop descriptions
COleDropTargetEx.cpp, COleDropTargetEx.h
Class COleDropTargetEx
:
- Support for drag images and drop descriptions
- Supports user defined drop descriptions
- Auto scroll support when dragging over inset regions and scroll bars
- Functions to get text data including extracting the content from HTML clipboard format data
- Functions to get image data (
CF_BITMAP
, CF_DIB
, CF_DIBV5
, drag image, TIFF, JPEG, GIF, PNG) - Functions to get file names from
CF_HDROP
DragDropHelper.cpp, DragDropHelper.h
- Class
CDragDropHelper
with static
helper functions - Class
CDropDescription
to store user defined drop descriptions (used by COleDataSourceEx
and COleDropTargetEx
) - Class
CHtmlFormat
to convert HTML strings to the HTML clipboard format and vice versa - Class
CTextToRtf
to convert plain text strings to RTF
DragDropHelper.h
Common definitions and definitions from newer SDK header files so that the sources can be build with Visual Studio 2005 and later.
OleDataDemo.cpp, OleDataDemo.h
The demo application.
OleDataDemoDlg.cpp, OleDataDemoDlg.h
The main dialog of the demo application.
MyListCtrl.cpp, MyListCtrl.h
CListCtrl
derived report style list:
- Clipboard and drag source providing plain text, CSV and HTML format
- Provides plain text, CSV and HTML as virtual files when dragging
- Drag target for single line plain text (dropped into single cell)
- Auto scrolling when dragging over scroll bars
MyEdit.cpp, MyEdit.h
CEdit
based multi line edit control:
- Drag & drop as source and target including dragging inside the control
- Accepts file names dragged from Explorer
- Context menu with Paste Special sub menu (CSV, HTML, and RTF as text; file names and file content)
- Auto scroll when dragging over scroll bars and inset region
MyStatic.cpp, MyStatic.h
CStatic
based picture control:
- Drag & drop as source and target for
bitmap
s - Loads images from file when dragging image files from the Explorer
- Drops the drag image if no
bitmap
provided by source - Provides
bitmap
as virtual file when dragging
InfoList.cpp, InfoList.h
CListCtrl
derived report style list:
- Shows information about clipboard and drag & drop data
- Data is updated when new data is put on the clipboard (clipboard viewer) or dropping on the list
- Uses user defined drag description text with special cursor
FileList.cpp, FileList.h
CMyListCtrl
derived class to list the files in directories in a similar way like the Windows Explorer.
Rich edit controls already support drag & drop, but without drag images. Trying to register a COleDropTarget
with rich edit controls will fail because it has been already registered as drop target. To overcome this and use your own drop implementation, the control must be unregistered as drop target using RevokeDragDrop
:
void CMyRichEditCtrl::Init()
{
if (NULL == m_pDropTarget)
{
::RevokeDragDrop(GetSafeHwnd());
VERIFY(m_DropTarget.Register(this));
}
}
With Windows Vista and later, and enabled UAC, dragging data between applications that are executed by users with different privileges is not possible.
The RegisterClipboardFormat()
description in the MSDN states: "When registered clipboard formats are placed on or retrieved from the clipboard, they must be in the form of an HGLOBAL value." This seems to be only valid for classic clipboard operations. With OLE operations, even Microsoft uses IStreams with registered formats.
As already noted, most parts of the new style drag cursor support are rarely documented. Please post a comment if you know something not mentioned in this article.
Besides the MSDN documentation for the MFC OLE classes and the OLE interfaces, these links may be of interest:
- 15th March, 2015
- 16th March, 2015
- Added missing files to source download archive
- Minor article changings (typos, uppercase title, tags, added missing info about FileList and image support)
- 18th April, 2016
- Fixed typos
- Changed style of callout boxes
- Added link to Windows Clipboard Formats
- 21st May, 2016
- Added information about registering richt edit controls as drop targets
- 20th July, 2017
- Clarification on auto deletion of
COleDataSource
and why the default SetData
fails