Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Drag & Drop Images and Drop Descriptions for MFC Applications

4.94/5 (44 votes)
26 Mar 2021CPOL33 min read 101.6K   8K  
Add drag & drop support with drag images and drop descriptions to your MFC applications
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.

OleDataDemo application screenshot

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.

Preparing to Use the MFC OLE Classes

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().

Providing Data for Clipboard and Drag & Drop

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:

C++
// Cache text string
bool COleDataSourceEx::CacheString(CLIPFORMAT cfFormat, LPCTSTR lpszText)
{
    HGLOBAL hGlobal = NULL;
    size_t nSize = (_tcslen(lpszText) + 1) * sizeof(TCHAR);
    if (nSize > sizeof(TCHAR))                       // don't cache empty strings
    {
        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:

C++
void CMyWnd::OnCopy()
{
     // A function that gets the selected text into a CString.
     // Returns empty string if there is no selection.
     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:

C++
// Cache locale information.
bool COleDataSourceEx::CacheLocale(LCID nLCID /*= 0*/)
{
    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.

C++
void CMyWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
    bool bHandled = false;                    // set when message processed by dragging
    bool bSimulateClick = false;              // set when a single click should be simulated
    // Window must have focus or selection must be always shown.
    // Some controls did not show the selection without having the focus
    //  (e.g. edit controls without style ES_NOHIDESEL).
    // Then check if clicked on the selection.
    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();
            // When moving is not allowed, pass DROPEFFECT_COPY only.
            DROPEFFECT dwEffect = DROPEFFECT_COPY;
            if (IsWindowEnabled() && !IsReadOnly())
               dwEffect |= DROPEFFECT_MOVE;
            // Clear the selection when DROPEFFECT_MOVE is returned.
            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); // default handling of left mouse button down
        if (bSimulateClick)                   // simulate single click
            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.

Pasting Data From the Clipboard

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().

C++
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));
        // Replace selection with text from clipboard or insert text
        //  at the current cursor position if nothing is selected.
        ReplaceSel(lpszText, 1);
        ::GlobalUnlock(hGlobal);
        ::GlobalFree(hGlobal);
    }
    // The clipboard is detached by the COleDataObject destructor
}

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 strings. 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.

Dropping Data

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.

Registering Drop Target Windows

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:

C++
void CMyEdit::Init(LPCTSTR lpszText /*= NULL*/, unsigned nLimitSize /*= 0*/)
{
    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):

C++
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:

C++
DROPEFFECT COleDropTargetEx::OnDragEnter
(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point)
{
    DROPEFFECT dwRet = DROPEFFECT_NONE;
    if (m_pOnDragEnter)                             // Using callback function
        dwRet = m_pOnDragEnter(pWnd, pDataObject, dwKeyState, point);
    else if (m_bUseMsg)                             // Using messages
    {
        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                                            // Default handling (CView support)
        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.

Implementing Drop Handlers

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:

C++
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)
    {
        // Set callback functions and register as drop target.
        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);
    // Call the non static version. Optional perform the work here.
    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:

C++
class CMyWnd2 : public CWnd
{
protected:
    virtual void PreSubclassWindow();
    LRESULT OnDropEx(WPARAM wParam, LPARAM lParam);
    // ...
};

// Message map
ON_MESSAGE(WM_APP_DROP_EX, OnDropEx)
// ...

void CMyWnd2::PreSubclassWindow()
{
    // Enable usage of messages and register as drop target.
    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)
    {
        // Handle dropping here.
    }
    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.

Drag Images

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.

Providing Drag Images

On the source side, we use the IDragSourceHelper interface by adding a member variable to our COleDataSourceEx class and initialise it in the constructor:

C++
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:

C++
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:

C++
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);    // delete image to avoid a memory leak
    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.

Creating Drag Images

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:

C++
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):

Drag image captured from screen

Drag image created by drawing text

Drag image created by drawing text

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.

Showing Drag Images

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:

C++
class COleDropTargetEx : public COleDropTarget
{
protected:
    IDropTargetHelper*    m_pDropTargetHelper;      // Drag image helper
};

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)
{
    // Call handler of pWnd here.
    // ...
    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.

Drop Descriptions (New Style Drag Cursors with Optional Text)

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.

Showing Drop Descriptions

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:

C++
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:

C++
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:

C++
DROPEFFECT COleDataSourceEx::DoDragDropEx
(DROPEFFECT dwEffect, LPCRECT lpRectStartDrag /*= NULL*/)
{
    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:

C++
class COleDropSourceEx : public COleDropSource
{
public:
    COleDropSourceEx();

protected:
    bool            m_bSetCursor;   // internal flag set when Windows cursor must be set
    LPDATAOBJECT    m_pIDataObj;    // set by COleDataSourceEx to its IDataObject

    bool            SetDragImageCursor(DROPEFFECT dwEffect);
    virtual SCODE   GiveFeedback(DROPEFFECT dropEffect);
public:
    friend class COleDataSourceEx;
};

COleDropSourceEx::COleDropSourceEx()
{
    m_bSetCursor = true;            // Must set default cursor
    m_pIDataObj = NULL;             // IDataObject of COleDataSourceEx
}

The function that updates the drag image cursor is the GiveFeedback() function:

C++
// Handle feedback.
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)
            {
                // NOTE:
                //  Add '#define OEMRESOURCE' on top of stdafx.h.
                //  This is necessary to include the OCR_ definitions from winuser.h.
                HCURSOR hCursor = (HCURSOR)::LoadImage(
                    NULL,                           // hInst must be NULL for OEM images
                    MAKEINTRESOURCE(OCR_NORMAL),    // default cursor
                    IMAGE_CURSOR,                   // image type is cursor
                    0, 0,
                    LR_DEFAULTSIZE | LR_SHARED);
                ::SetCursor(hCursor);
                m_bSetCursor = false;
            }
            SetDragImageCursor(dropEffect);         // Select and show new style drag cursor
            sc = S_OK;                              // Don't show default (old style) cursor
        }
        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:

C++
// Select drag image cursor
bool COleDropSourceEx::SetDragImageCursor(DROPEFFECT dwEffect)
{
    // Stored data is always a DWORD even with 64-bits apps.
    HWND hWnd = (HWND)ULongToHandle(GetGlobalDataDWord(_T("DragWindow")));
    if (hWnd)
    {
        WPARAM wParam = 0;  // Use DropDescription to get type and optional text
        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.

Changing Drop Descriptions

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:

C++
SCODE COleDropSourceEx::GiveFeedback(DROPEFFECT dropEffect)
{
    SCODE sc = COleDropSource::GiveFeedback(dropEffect);
    if (m_bDragStarted && m_pIDataObj)
    {
        bool bOldStyle = 
            (0 == CDragDropHelper::GetGlobalDataDWord(m_pIDataObj, _T("IsShowingLayered")));

        // Check if the drop description data object must be updated:
        // - When entering a window that does not show drag images while the previous
        //   target has shown the image, the drop description should be set to invalid.
        //   Condition: bOldStyle && !m_bSetCursor
        // - When setting drop description text here is enabled and the target
        //   shows the drag image, the description should be updated if not done by the target.
        //   Condition: m_pDropDescription && !bOldStyle
        if ((bOldStyle && !m_bSetCursor) || (m_pDropDescription && !bOldStyle))
        {
            FORMATETC FormatEtc;
            STGMEDIUM StgMedium;
            if (CDragDropHelper::GetGlobalData(m_pIDataObj, CFSTR_DROPDESCRIPTION, 
                                               FormatEtc, StgMedium))
            {
                bool bChangeDescription = false; // Set when DropDescription changed here
                DROPDESCRIPTION *pDropDescription = 
                    static_cast<DROPDESCRIPTION*>(::GlobalLock(StgMedium.hGlobal));
                if (bOldStyle)
                    bChangeDescription = CDragDropHelper::ClearDescription(pDropDescription);
                // The drop target is showing the drag image and new cursors 
                // and may have changed the description.
                else if (pDropDescription->type <= DROPIMAGE_LINK)
                {
                    DROPIMAGETYPE nImageType = CDragDropHelper::DropEffectToDropImage(dropEffect);
                    // When the target has changed the description, 
                    // it usually has set the correct type.
                    // If the image type does not match the drop effect, set the text here.
                    if (DROPIMAGE_INVALID != nImageType &&
                        pDropDescription->type != nImageType)
                    {
                        // When text is available, set drop description image type and text.
                        if (m_pDropDescription->HasText(nImageType))
                        {
                            bChangeDescription = true;
                            pDropDescription->type = nImageType;
                            m_pDropDescription->CopyText(pDropDescription, nImageType);
                        }
                        // Set image type to invalid to use default cursor and text.
                        else
                        {
                            bChangeDescription = 
                                CDragDropHelper::ClearDescription(pDropDescription);
                        }
                    }
                }
                ::GlobalUnlock(StgMedium.hGlobal);
                if (bChangeDescription)         // Update the DropDescription data object when
                {                               //  type or text has been changed here.
                    if (FAILED(m_pIDataObj->SetData(&FormatEtc, &StgMedium, TRUE)))
                        bChangeDescription = false;
                }
                if (!bChangeDescription)        // Description not changed or setting it failed
                    ::ReleaseStgMedium(&StgMedium);
            }
        }
        if (!bOldStyle)                         // Show new style drag cursors.
        {
            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;                          // Don't show old style drag cursor
        }
        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:

User defined cursor and text for list content from demo app

User defined cursor and text for text file from 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)
  1. Data object is only present when set once. If not present, boolean and DWORD values should be treated as false / zero.
  2. Used when dragging from Explorer.
  3. 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.

Using the Code

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 bitmaps
  • 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.

Points of Interest

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:

C++
void CMyRichEditCtrl::Init()
{
    if (NULL == m_pDropTarget)
    {
        // RichEdit controls are already registered as drop target.
        // So unregister here before registering our class.
        ::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.

References

Besides the MSDN documentation for the MFC OLE classes and the OLE interfaces, these links may be of interest:

History

  • 15th March, 2015
    • Initial version
  • 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)