65.9K
CodeProject 正在变化。 阅读更多。
Home

为 MFC 应用程序添加拖放图像和拖放描述

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (43投票s)

2015 年 3 月 15 日

CPOL

33分钟阅读

viewsIcon

103938

downloadIcon

8057

为您的 MFC 应用程序添加支持拖放图像和拖放描述的功能

OleDataDemo application screenshot

引言

本文介绍如何为 MFC 应用程序添加拖放支持。文章将从介绍如何使用剪贴板和拖放操作开始,其中包含其他来源找不到的有用技巧和扩展。重点在于拖放描述。拖放描述可用于显示 Windows Vista 中引入的具有可选文本的 Aero 拖放光标。由于拖放描述需要应用程序支持拖放图像,因此也将详细解释这一点。

本文处理的主题包括

本文中的所有代码均使用封装低级 OLE 数据接口的 MFC OLE 类。为了实现所需功能,我们将从这些 MFC 类派生自己的类。本文中展示的代码片段基于源代码,但已通过删除注释、错误检查和不必要的信息进行了缩短。

准备使用 MFC OLE 类

要使用 MFC OLE 类,必须从 InitInstance() 调用初始化函数 AfxOleInit(),并且必须包含头文件 afxole.h(最好放在 stdafx.h 文件中)。由于忘记初始化 OLE 是一个常见的导致意外行为的错误,我将稍微强调一下:

注意:不要忘记调用 AfxOleInit()

为剪贴板和拖放提供数据

COleDataSource 类包含将数据放入剪贴板和启动拖放操作所需的所有函数。这两种方法的准备数据方式相同。因此,最好为通用的剪贴板格式提供一些数据准备函数。

COleDataSource 派生我们的类 COleDataSourceEx 后,我们可以添加一个函数来准备纯文本数据:

// 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;
}

此函数会分配一个全局内存对象,将 string(包括终止 NULL 字符)复制到其中,最后缓存要设置到剪贴板或稍后用作拖放源的数据。请注意,使用全局内存对象作为数据源时,必须使用 GMEM_MOVEABLEGHND 进行分配。要访问此类已分配的数据,必须使用 GlobalLock() 函数,并在完成后解锁内存。

现在,我们可以从任何窗口类中使用我们的类和此函数将文本字符串复制到剪贴板:

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();
     }
}

就这样,很简单。但有一个陷阱,微软文档并未对此说清楚:

注意:数据源对象必须使用 new 在堆上分配,并且调用 FlushClipboard() 后不得删除或释放!

原因是 FlushClipboard() 函数调用 ::OleFlushClipboard(),后者会为封装的 IDataObject 调用 Release(),最终导致对象被删除。调用 COleDataSource::InternalRelease 而不是刷新剪贴板时,对象也会被删除,但数据将不再位于剪贴板上。如果不调用任何一个,当任何地方放置新数据到剪贴板时,对象就会被删除。

提供 CF_TEXTCF_OEMTEXT 时,文本的代码页必须已知,以便在接收应用程序使用 Unicode 或其他代码页时进行转换。为了指定编码,将 ANSI 或 OEM(MS-DOS)文本放入剪贴板或通过拖放提供此类文本的应用程序,还应提供一个包含创建文本时使用的 LCIDCF_LOCALE 对象。因此,我们可以添加另一个函数来缓存 LCID

// 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;
}

LCID 传递零时,将使用线程的区域设置。ConvertDefaultLocale() 在传递 LOCALE_USER_DEFAULTLOCALE_SYSTEM_DEFAULT 时返回有效的 LCID

注意:提供 CF_TEXTCF_OEMTEXT 数据时,也请提供 CF_LOCALE 格式。

现在,我们使用相同的数据进行拖放,并传递 LCID。这通常在按下鼠标左键时启动。因此,我们必须在窗口类中处理 WM_LBUTTONDOWN 消息。有两个控件已经内置了拖放开始检测:列表控件和树形视图控件。对于这些控件,分别处理 LVN_BEGINDRAGTVN_BEGINDRAG 消息。

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));
    }
}

在何时开始拖放以及何时不开始拖放时必须小心。一些一般规则是:

  • 控件中必须有选中项,或者默认拖动全部内容。
  • 当必须有选中项时,单击必须发生在选中项上。
  • 当必须有选中项且控件没有焦点时,不显示选中项,则控件必须具有焦点。

最后一点适用于大多数控件。默认情况下,当控件没有焦点时,它们不会显示当前选中项。对于编辑控件,可以通过在创建控件之前设置 ES_NOHIDESEL 样式来更改此行为。

请注意,数据以 CF_TEXTCF_UNICODETEXT 格式提供。对于剪贴板操作,如果不存在所请求的文本格式但存在任何其他格式(ANSI、Unicode 或 OEM/DOS),系统会返回转换后的数据。但这不适用于拖放操作。因此,我们应该提供所有可能的格式(此处排除 CF_OEMTEXT)。

注意:在拖放操作中,检索数据时,标准剪贴板格式没有隐式转换。

您可能注意到了 InternalRelease() 函数调用。此调用对于删除对象是必需的。虽然在拖放操作中使用 delete 或在堆栈上创建对象是可能的,但通常不推荐这样做。

注意:启动拖放操作时,COleDataSource 对象不会自动销毁。

还要注意,当拖放操作已经开始时(即使已取消),默认的鼠标按下处理程序也不会被调用。这是为了确保控件窗口的内部鼠标按钮状态处于定义状态。调用 DoDragDrop() 时,所有后续鼠标事件(特别是鼠标抬起事件)都会被捕获,直到函数返回。但有一个特殊情况:当拖放操作尚未开始时。当鼠标离开一个定义的区域(默认情况下为 1 像素宽的矩形)或经过一段特定时间(默认情况下为 200 毫秒)后,拖放操作最终启动。如果通过立即释放鼠标按钮未启动拖放操作,此单击可能会通过调用默认处理程序并在之后发送鼠标抬起消息来传递给控件。由于 COleDataSource 类未提供检测此情况的方法,因此已在 COleDataSourceEx 类中实现,调用 DoDragDropEx() 时。有关示例,请参见 CMyEdit::OnLButtonDown() 源代码。

注意:当拖放操作已经开始时,请勿调用默认的鼠标按下处理程序。

如上所述,拖放操作开始前存在一个延迟时间。如果应立即开始拖放操作而不等待,请传递一个指向 null 矩形的指针(即所有成员均设置为零的矩形)。MSDN 中未提及此点。

从剪贴板粘贴数据

要访问剪贴板中的数据,请使用 COleDataObject 类并附加剪贴板。这是一个 CEdit 派生类的简单示例。这只是一个示例;实际应用程序将仅使用 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));
        // 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
}

注意最后的 GlobalFree() 调用以删除内存。COleDataObject::GetGlobalData() 返回的全局内存句柄始终是一个新的内存块,它已分配并填充了源端传递的内存的副本。使用 COleDataObject::GetData() 时,之后调用 ReleaseStgMedium() 以释放数据。MSDN 文档对此未明确说明。

注意:检索数据时,不要忘记释放全局内存和介质。

标准文本格式 CF_TEXTCF_OEMTEXTCF_UNICODETEXT 必须包含 null 终止的 string。但这不适用于 CSV、RTF 等其他文本格式。并且可能有一些应用程序即使对于标准文本格式也没有复制 null 终止符。因此,检查这一点可能很有用。可以使用 GlobalSize() 函数来获取已分配内存的大小,并在 string 未以 null 终止时限制长度。请参见源代码中的 COleDropTargetEx::GetString() 函数。它会将文本数据复制到具有大小检查的已分配内存中,并在必要时使用 CF_LOCALE 执行 Unicode/代码页转换。

注意:在从剪贴板或通过拖放获取文本数据时,不要依赖 null 终止的字符串。

拖放数据

MFC 提供了 COleDropTarget 类以允许窗口接受拖放命令。但该类仅支持 CView 派生类。因此,我们必须通过使用我们自己的 COleDropTarget 派生类来实现对其他类的拖放支持。由于稍后将向此类添加对拖放图像的支持,因此也应将其用于 CView 类。

注册拖放目标窗口

要将窗口注册为拖放目标,请向窗口类添加一个 COleDropTarget 成员,并调用 Register() 函数,传递指向该窗口的 CWnd 指针。通常在 OnInitialUpdate()(视图窗口)、OnInitDialog()(对话框窗口)以及 OnCreate()(所有其他窗口)中执行此操作。

但是,对于模板创建的窗口(例如,在加载对话框模板时创建的控件),OnCreate() 不会被调用(窗口在 CWnd 包装器之前创建,因此 CWnd::OnCreate() 永远不会被调用)。对于此类控件,可以通过重写 CWnd 的虚拟函数 PreSubclassWindow() 或在父对话框的 OnInitDialog() 中进行注册。在这种情况下,控件窗口应提供一个初始化函数,该函数注册窗口并可选择执行其他任务。例如,一个 CEdit 派生类的初始化函数,它还可以设置初始内容和文本限制:

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);
}

CWnd 成员变量 m_pDropTargetNULL 时,窗口将被注册为拖放目标。此变量在注册时被设置为 COleDropTarget 的地址。CWnd::OnNcDestroy() 使用此变量来撤销(注销)窗口。在此检查 NULL 对于避免在再次调用初始化函数时出现调试版本中的断言是必需的。

注意:对于模板创建的窗口,CWnd::OnCreate() 不会被调用。

对非 CView 类窗口的拖放支持

问题在于 COleDropTarget 类的拖放事件处理程序必须调用窗口类的相应函数。COleDropTarget 通过检查传递的 CWnd* 是否为 CView 类型来解决此问题。如果检查成功,CWnd* 将被转换为 CView*,并调用相应的虚拟处理程序函数(此处为 OnDragEnter)。

    if (!pWnd->IsKindOf(RUNTIME_CLASS(CView)))
        return DROPEFFECT_NONE;
    CView* pView = (CView*)pWnd;
    ASSERT_VALID(pView);
    return pView->OnDragEnter(pDataObject, dwKeyState, point);

此技术也可用于其他窗口。但它需要包含头文件并为所有必须支持的类添加类似的代码。我不喜欢这种解决方案,原因有二:拖放目标类依赖于支持的窗口类,因此是项目特定的,并且它使用了 IsKindOf(),应该避免使用(至少在发布代码中)。

替代选项是使用回调函数或发送用户定义的自消息。我决定实现这两种方法:

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;
}

有关其他处理程序的更多信息,请参见 COleDropTargetEx 源代码。

使用回调函数时,目标窗口必须在注册之前传递它们。由于回调函数必须是静态的,因此目标窗口类必须将其传递的 CWnd* 参数转换为其类,并使用此指针访问成员或调用非静态版本的函数。演示应用程序中的 CMyListCtrl 类使用了此方法。

成员变量 m_bUseMsg 指定拖放目标窗口是否处理用户定义的自消息用于拖放事件。它必须在注册之前由目标窗口设置。由于大多数拖放事件有三个以上的参数,我们必须使用 DragParameters 结构来传递它们。此结构和基于 WM_APP 的消息代码定义在 COleDropTargetEx.h 中。演示应用程序中的 CMyEdit 类使用了此方法。

如果既不发送自消息也不使用回调函数,则会调用基类的默认处理程序。这将调用 CView 类派生类的虚拟处理程序函数。

实现拖放处理程序

并非所有处理程序函数都必须实现。对于基本的拖放支持,请添加 OnDragEnter()OnDragOver()OnDropEx()OnDrop()

OnDragEnter

当第一次(或在离开后再次)拖动到窗口上方时调用此函数。首先,应检查窗口是否能够拖放数据(窗口已启用且可选的只读状态未设置)。然后检查拖放源是否提供可拖放的数据。其他检查可能包括提供的数据是否合适(例如,文本字符数是否小于指定限制)。这些检查的结果可以存储在成员变量中,供 OnDragOver() 使用。返回的拖放效果应像 OnDragOver() 返回的效果一样确定。

OnDragOver

主要目的是根据按键状态设置拖放效果(例如,按住 Control 键时复制,否则移动),并检查数据是否可以拖放到当前位置(例如,在客户区上方,而不是在滚动条或列表控件的标题上)。当在窗口中移动鼠标时(类似于 WM_MOUSEMOVE 消息)以及按键状态(**Shift**、**Ctrl**、**Alt**)更改时,会反复调用此函数。因此,请勿在此函数中执行耗时任务。在调用 OnDropEx()OnDrop() 之前也会调用此函数,以获取要传递给这些函数的拖放效果。由于返回的拖放效果用于确定光标类型并 upon dropping 返回给源,因此它应该是一个单一效果,而不是多个效果的组合。

OnDragLeave

离开窗口时调用此函数。用于清理。对于大多数窗口,没有必要实现此处理程序。如果存在以执行某些清理,则可能需要向拖放处理程序添加类似代码,因为在发生拖放时不会调用 OnDragLeave()

OnDrop 和 OnDropEx

只需实现其中一个函数。在窗口上方释放鼠标按钮时调用它们。在此处获取数据并在应发生拖放时将其插入控件。释放鼠标按钮时,将调用这些函数:

  • 调用 OnDragOver() 以获取要传递给 OnDropEx() 和/或 OnDrop() 的拖放效果。
  • 调用 OnDropEx()(即使拖放效果为 DROPEFFECT_NONE)。
  • 如果 OnDropEx() 返回 -1 且拖放效果不为 DROPEFFECT_NONE,则调用 OnDrop()
  • 如果 OnDropEx() 返回 -1 且拖放效果为 DROPEFFECT_NONE,则调用 OnDragLeave()

最终传递给源的 DoDragDrop() 函数的拖放效果是:

  • 如果 OnDropEx() 返回的值不为 -1
  • 否则,如果 OnDrop() 返回 TRUE,则为 OnDragOver() 返回的值,
  • 否则为 DROPEFFECT_NONE

由于即使拖放效果为 DROPEFFECT_NONE,也会始终调用 OnDropEx(),因此 OnDropEx() 应检查传递的值并在此时返回而不进行拖放。

OnDragScroll

可以处理此选项以在拖动经过滚动条或插入区域(客户区边界内的小带)时执行自动滚动。OnDragScroll() 是进入或移动经过窗口时调用的第一个处理程序。滚动激活时,将返回值中的 DROPEFFECT_SCROLL 位设置为 1,以避免进一步处理。如果未设置滚动位,则调用 OnDragEnter()OnDragOver()

COleDropTarget 支持视图窗口的插入区域的滚动。源代码中的 COleDropTargetEx 类提供了默认处理,当应执行滚动时,它会调用回调函数或发送用户定义的自消息。通过一次调用配置函数并传递滚动源(水平滚动条、垂直滚动条和插入区域)和回调函数的标志来启用默认处理。演示程序中的 CMyEditCMyListCtrl 类使用了此选项。

拖放处理程序作为回调函数

使用回调函数时,请将 static 函数添加到窗口类中,并在注册之前将它们的地址传递给拖放目标类。

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);
}

拖放处理程序用于用户定义的自消息

使用用户定义的自消息时,请添加返回 LRESULT 并接受 WPARAMLPARAM 参数的标准自消息处理程序,并在注册之前启用自消息的使用。

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;
}

自消息处理程序必须将 WPARAM 参数转换为 COleDataObject*,并将 LPARAM 参数转换为 DragParameters*

注意:使用此方法时,必须始终存在 WM_APP_DROP_EX 处理程序。如果它不处理事件,则必须返回 -1 以指示此情况。

基于 CView 的窗口的拖放处理程序

对于 CView 类,只需重写虚拟的拖放事件函数。但是,也可以使用其他方法。

拖放图像

在 Windows 2000 中,引入了拖放图像,允许在拖放操作期间显示 alpha 混合图像。此类图像必须由拖放源提供,拖放目标负责显示它们。

提供拖放图像

在源端,我们通过向 COleDataSourceEx 类添加成员变量来使用 IDragSourceHelper 接口,并在构造函数中对其进行初始化:

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 提供了两个函数来指定拖放图像。因此,我们向类添加相应的 public 函数:

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);
}

使用此函数时,传递 HWND 的控件窗口负责创建拖放图像。它必须响应已注册的 DI_GETDRAGIMAGE 消息并填充传递的 SHDRAGIMAGE 结构。列表控件和树形视图控件已包含此类消息处理程序,因此对于这些控件无需进一步操作。但对于报表模式下的列表控件,只有第一列会复制到图像中,而图像宽度则准备容纳所有列。可以将 NULL 传递给 HWND 参数。在这种情况下,系统将生成一个通用图像。如果存在全局数据对象“**Shell IDList Array**”,则将其用于确定拖放图像(就像从 Explorer 拖动文件时一样)。

如果控件窗口不处理 DI_GETDRAGIMAGE 消息,则可以通过将位图作为拖放图像传递来指定拖放图像:

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);
}

此函数将位图作为拖放图像传递。pPoint 指定光标相对于图像左上角的偏移量,clr 指定图像的透明颜色。拖放助手将拥有该位图,并在拖放完成后释放它。

此时,我应该解释透明度是如何实现的:图像中透明颜色的每个像素都会变为黑色,相应的位用于生成蒙版。因此,如果透明颜色不是黑色,拖放图像不应包含黑色像素。否则,这些像素将不可见。

注意:黑色像素在拖放图像中不可见。

当现在执行这些函数时,调用 IDragSourceHelper 函数将因 DATA_E_FORMATETC(“无效 FORMATETC 结构”)而失败。原因是 COleDataSource::XDataObj::SetData() 函数尝试在第二个(设置)缓存中查找现有条目。但该缓存默认为空,并且必须实现向其中添加数据。但即使这样做也无济于事,因为对于剪贴板和拖放操作,我们必须使用第一个(获取)缓存。因此,我们必须重写此函数并缓存新数据或更新现有数据。由于 IDataObject 接口函数是纯虚拟函数,因此我们必须添加所有这些函数,而不仅仅是 SetData() 函数。

有关更多信息,请参见 Carsten Leue 的文章 DragSourceHelper MFC。源代码基于该文章。

创建拖放图像

现在可以传递拖放图像了,我们如何创建它们?根据源控件的类型,有许多选项。最简单的解决方案(从代码的角度来看)是从资源加载它们:

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;
}

要获取特定文件类型的图标,请使用 SHGetFileInfo() 函数。它会填充一个 SHFILEINFO 结构,其中包含与指定文件名匹配的图标。

本文的 COleDataSourceEx 类源代码提供了一些创建拖放图像的函数:

  • CBitmap
  • 从资源加载的 bitmap
  • 从通过指定文件名扩展名检索到的文件类型图标
  • 使用 CWnd 的字体和大小从文本字符串创建
  • 通过捕获 CWnd 的一个区域并可选地缩放
  • 通过复制 bitmap 并可选地缩放(拖动位图时很有用)

以下是从列表控件拖动时(从窗口捕获,使用透明灰色文本和系统选择颜色从文本生成)的演示应用程序的一些示例:

Drag image captured from screen

Drag image created by drawing text

Drag image created by drawing text

如上所述,当透明颜色不是黑色时,图像不应包含黑色像素。但可能存在包含黑色像素的图像源。为了处理这些情况,COleDataSourceEx 类提供了一个函数,用于将高彩色位图的黑色像素替换为最接近的颜色。

您可能已经看到过被虚化的拖放图像(图像的外围区域被淡出)以及显示原样的图像。当拖放图像的宽度或高度超过 300 像素时,系统会执行这种虚化。

显示拖放图像

要在窗口上方显示拖放图像,窗口必须调用 IDropTargetHelper 接口的相应拖放事件处理程序函数。为了接收拖放事件,此类窗口必须使用 COleDropTarget 类将自己注册为拖放目标。因此,我们可以简单地将 IDropTargetHelper 成员添加到我们的 COleDropTargetEx 类中,在构造函数中对其进行初始化,并调用相应的处理程序函数:

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;
}

有关其他事件处理程序,请参阅本文的源代码。m_lpDataObject 是一个指向 OLE 数据对象接口的 COleDropTarget 成员。COleDataObject 参数已附加到此对象。由于我们必须将其传递给拖放目标助手,因此我们可以直接使用成员变量,而不是通过 GetIDataObject() 从参数中获取。

就这样。现在,使用我们的 COleDropTargetEx 类并将其注册为拖放目标的每个窗口,在拖动到它们上方时都会显示拖放图像。

但那些未处理拖放事件的窗口呢?

为了在应用程序的任何窗口上方显示拖放图像,请向应用程序的主窗口添加一个 COleDropTargetEx 成员,并将其注册为拖放目标。然后,该窗口将处理事件并在没有客户端窗口执行此操作时显示拖放图像。对于对话框应用程序,此窗口是主对话框,注册应在 OnInitDialog() 中完成。对于基于主框架的应用程序,请为此主框架窗口类执行此操作,并在 OnCreate() 中进行注册。当主窗口不应支持拖放时,无需添加拖放事件处理程序。

提示:要在应用程序的任何窗口上方显示拖放图像,请向应用程序的主窗口添加所需处理程序。

拖放描述(带可选文本的新样式拖放光标)

在 Windows Vista 中,引入了拖放描述,提供了带可选文本的新 Aero 光标。一个例子是 Windows Explorer,它在拖动文件时显示带文本的新光标。但此功能很少有文档记录。因此,这里将对其进行详细解释。

显示拖放描述

默认情况下,在使用新光标时不会显示文本。要启用文本,已从 IDragSourceHelper 继承了一个新接口,该接口提供了另一个函数来启用拖放光标的文本显示。因此,我们应首先更新 COleDataSourceEx 构造函数:

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);
        }
    }
}

创建拖放源助手后,代码将尝试访问 IDragSourceHelper2 接口。如果成功,我们就可以使用新光标(成员变量 m_pDragSourceHelper2 现在也可以用来指示应用程序运行在 Vista 或更高版本上)。由于新接口继承自旧接口,因此在释放旧接口后可以分配新接口的指针。

现在我们可以访问新函数来启用文本:

bool COleDataSourceEx::AllowDropDescriptionText()
{
    return m_pDragSourceHelper2 ? 
        SUCCEEDED(m_pDragSourceHelper2->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT)) : false;
}

必须在调用 InitializeFromWindow()InitializeFromBitmap() 之前调用此函数。否则,将不会显示文本。

要显示新光标,拖放目标窗口必须显示拖放图像,并且拖放图像窗口本身必须在拖动期间的每次鼠标移动时进行无效化。此无效化应由拖放源执行。

为了在拖动操作期间的每次鼠标移动时获得通知,我们将从 COleDropSource 派生一个类,该类实现虚拟函数 GiveFeedback()。每当拖放目标处理程序返回时,就会调用此函数。然后将此派生类的局部实例传递给 COleDropSource::DoDragDrop() 函数:

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));
}

当可以使用拖放描述时,将 OLE 数据对象接口的指针传递给它以访问缓存的数据(COleDataSource 类仅提供写入数据的函数,但我们也必须读取数据对象)。拖放描述可与 Vista 或更高版本(存在 IDragSourceHelper2 接口)和启用的视觉样式一起使用。IsAppThemed() 需要链接 uxtheme.lib(在 COleDataSourceEx 中,这通过 pragma 语句完成)。如果应用程序必须支持 XP 之前的 Windows 版本,则必须通过将其添加到相应的项目链接器设置中来延迟加载 uxtheme.dll

现在让我们看看我们的 COleDropSourceEx 类:

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
}

更新拖放图像光标的函数是 GiveFeedback() 函数:

// 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;
}

调用基类函数后,我们检查是否可以使用拖放描述。这需要拖放已启动,并且 COleDataSourceEx 已初始化接口成员变量。下一步是读取布尔值“IsShowingLayered”数据对象。CDragDropHelper::GetGlobalDataDWord() 是一个辅助函数,它使用 IDataObject::GetData() 读取存储在全局数据对象中的 DWORD 值。“IsShowingLayered”由 IDropTargetHelper 设置为 true,当拖放目标显示拖放图像时。如果为 false,则将显示旧样式的拖放光标。否则,通过调用 SetDragImageCursor(dropEffect) 显示对应拖放效果的默认新样式光标。但在执行此操作之前,我们必须将 Windows 光标设置为默认箭头。当进入显示拖放图像的拖放目标并且前一个目标未显示它时,这是必需的。否则,旧样式的拖放光标仍然可见。最后,我们必须将返回值更改为 SC_OK,以指示光标已更新,并且我们不想要旧样式光标。

现在来看 SetDragImageCursor() 函数:

// 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;
}

全局数据对象“DragWindow”在 DWORD 中存储了拖放图像窗口的句柄。要更新窗口,必须发送特定的自消息。SetDragImageCursor() 使用未文档化的自消息通过 WPARAM 参数指定光标类型:

WPARAM 描述
0 全局数据对象“DropDescription”定义了光标类型和可选文本:
1 使用停止标志光标,无文本(无法拖放)
2 使用箭头光标,带系统默认文本(移动)
3 使用加号光标,带系统默认文本(复制)
4 使用弯曲箭头光标,带系统默认文本(链接)

如果 WPARAM 为零或存在全局“DropDescription”数据对象且其图像类型成员有效,则使用它来定义光标类型和可选文本。否则,WPARAM 指定光标类型,可选文本是系统默认文本(就像从 Windows Explorer 拖动文件时显示的文本一样)。还有一个文档化的自消息,消息代码为 DDWM_UPDATEWINDOWWM_USER+3),WPARAMLPARAM 都设置为零。这将使用全局数据对象“DropDescription”中的光标和文本,类似于 WPARAM 为零的 WM_USER+2 消息。

更改拖放描述

上述示例使用带可选文本的默认拖放光标。但光标类型和文本也可以由拖放源和拖放目标指定。要实现这一点,必须创建全局“DropDescription”数据对象,或在它已存在时更改它。此数据对象使用有良好文档记录的 DROPDESCRIPTION 结构。在我们的 COleDropSourceEx::GiveFeedback() 函数内部,我们必须检查描述是否已被拖放目标更改。如果没有,我们可以在源端选择性地更改它。如果存在描述并且有效,则拖放图像窗口将使用它。否则,将根据拖放效果显示默认光标和文本。

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;
}

除了先前版本的反馈函数,我们现在还将获取 DropDescription 并检查是否需要更新它。如果存在且拖放目标未显示拖放图像,我们将清除描述。这样我们就可以检测到是否由另一个拖放目标更改了它。如果存在描述且拖放目标显示拖放图像,我们必须检测描述是否已由拖放目标更改。如果图像类型无效(DROPIMAGE_INVALID),我们可以确信描述未由目标设置。如果图像类型是此处不支持的类型之一(特殊类型,超出相应的拖放效果),我们可以确信描述已由拖放目标更改。在所有其他情况下,如果我们假设图像类型与拖放效果匹配,则描述已由拖放目标更改。

当描述存在并且不被目标更改时,我们可以在启用文本使用时在此处设置它。当禁用文本时,无需更改描述:图像类型定义了要使用的光标(当其为 DROPIMAGE_INVALID 时,对应于拖放效果的类型)。m_pDropDescription 是指向 COleDataSourceEx 成员的指针。它是一个帮助类,用于保存图像类型的拖放描述字符串。当没有定义文本时,指针将设置为 NULL

当描述存在且 szMessage 成员是一个空 string 时,图像类型应设置为无效以显示默认光标。如果不这样做,光标下方和右侧将有一个空的文本区域,这看起来可能不常见。

由于此反馈函数在每次鼠标移动时都会被调用,因此仅在必要时读取和更新描述。当描述存在且有效时,它将用于定义光标类型和可选文本(将忽略拖放图像窗口更新消息的 WPARAM)。否则,根据拖放效果选择图像。

在目标端更改拖放描述

要实现这一点,只需在 COleDropTargetEx 类的 OnDragEnter()OnDragOver() 中设置“DropDescription”数据对象,并在 OnDragLeave() 时使用 DROPIMAGE_INVALID 作为图像类型将其重置。要为某些效果显示用户定义的自消息,而为其他效果显示默认文本,请将拖放描述设置为无效,以用于应使用默认文本的效果。COleDropTargetEx 类提供了设置“DropDescription”数据的函数,并在离开时清除结构。在目标端设置拖放描述不仅能与我们的 COleDropSourceEx 类一起工作,还能与所有其他以这种方式处理拖放描述的拖放源(例如 Windows Explorer)一起工作。

示例应用程序中的 Info 列表使用了 DROPIMAGE_LABEL 类型并设置了用户定义的文本。这将如下所示(第一个来自演示应用程序的列表控件,第二个来自从 Explorer 拖动文本文件):

User defined cursor and text for list content from demo app

User defined cursor and text for text file from Explorer

更多需要注意的事项

OnDragEnter()OnDragOver() 处理程序通常会根据实际的按键状态返回拖放效果(例如,默认情况下是移动,按住 Control 键时是复制)。但此效果可能不受源支持(例如,仅支持复制)。因此,必须检查返回的效果并进行更改。COleDropTarget 类仅在从 OLE 接口处理程序返回之前进行此操作。但在更改拖放描述时,必须先进行过滤,以避免显示错误的光标和文本。由于从源传递的允许拖放效果 DoDragDrop() 不由 COleDropTarget 类存储,因此必须由 COleDropTargetEx 类检索并存储。这需要像 COleDataSource 类一样实现接口映射。

当存在拖放描述数据对象时,系统将始终使用它来在支持拖放图像的窗口上方显示带可选文本的新样式光标。当拖放源提供拖放图像但不支持新样式光标时(在 GiveFeedback() 函数中不更改 Windows 光标),这会导致屏幕上同时显示旧样式和新样式光标。因此,如果拖放目标不确定源是否支持新样式光标,则不应创建拖放描述数据对象。

与拖放图像和拖放描述一起使用的数据对象

使用拖放图像时,有许多数据对象包含特定信息(请使用演示应用程序在拖放到 Info 列表时查看它们)。大多数这些数据对象未文档化。这里的信息来自网络和自己的研究。

格式名称 类型 描述
ComputedImage DWORD ? (1, 2)
DisableDragText BOOL ? (1, 2)
DragContext IStream Windows 拖放助手内部使用。
DragImageBits SHDRAGIMAGE 拖放图像 bitmap。结构后面是图像位。
DragSourceHelperFlags DWORD 传递给 IDragSourceHelper2::SetFlags() 的值。当拖放描述文本不存在或为 false 时,可用于避免更新它。(1)
DragWindow DWORD 拖放图像窗口的 HWND,存储为 DWORD
DropDescription DROPDESCRIPTION 用于新样式光标的光标类型和文本。
IsComputingImage BOOL ? (1, 2)
IsShowingLayered BOOL 当拖放目标显示拖放图像时设置。
IsShowingText BOOL 当拖放目标请求更新新样式光标时设置。(3)
UsingDefaultDragImage BOOL 拖放图像是默认图像(例如,当将 NULL hWnd 传递给 IDragSourceHelper::InitializeFromWindow() 或窗口未响应 DI_GETDRAGIMAGE 消息时)。在活动拖放会话期间设置此值时,图像将从用户指定的图像更改为默认图像。(1)
  1. 仅当第一次设置时数据对象才存在。如果不存在,则布尔值和 DWORD 值应视为 false / zero。
  2. 从 Explorer 拖动时使用。
  3. 当不需要更新新样式光标时(例如,进入或离开拖放目标窗口或已拖放数据时),IsShowingText 被设置为 false。这可以从 GiveFeedback() 函数内部使用,以抑制光标更新,但光标可能会在极短的时间内不可见。

Using the Code

源代码支持 Unicode 构建和使用 ANSI(单字节)代码页的非 Unicode 构建。演示应用程序需要 Windows XP 或更高版本(调用 Theme API 函数而不检查 Windows 版本)。MFC OLE 派生类专为 Windows 2000 及更高版本设计。要编译源代码,需要 Visual Studio 2005 或更高版本。

源代码可能包含一些此处未提及的有趣函数。因此,我将简要概述提供的文件、类和支持的操作。

COleDataSourceEx.cpp, COleDataSourceEx.h

COleDataSourceEx

  • 支持拖放图像和拖放描述
  • 缓存和渲染纯文本、CSV、HTML 和 RTF 的函数
  • 缓存和渲染 CF_BITMAPCF_DIBCF_DIBV5 格式图像,并进行 DDB 到 DIB 的转换
  • 缓存和渲染 TIFF、JPEG、GIF 和 PNG 图像的函数
  • 从资源、位图、文本、文件类型图标和窗口区域创建拖放图像的函数
  • 缓存数据为虚拟文件的函数
  • 支持用户定义的拖放描述文本

COleDropSourceEx.cpp, COleDropSourceEx.h

COleDropSourceEx

  • 支持拖放描述

COleDropTargetEx.cpp, COleDropTargetEx.h

COleDropTargetEx

  • 支持拖放图像和拖放描述
  • 支持用户定义的拖放描述
  • 拖动经过插入区域和滚动条时的自动滚动支持
  • 获取文本数据的函数,包括从 HTML 剪贴板格式数据中提取内容
  • 获取图像数据的函数(CF_BITMAPCF_DIBCF_DIBV5、拖放图像、TIFF、JPEG、GIF、PNG)
  • CF_HDROP 获取文件名的函数

DragDropHelper.cpp, DragDropHelper.h

  • CDragDropHelper,包含 static 辅助函数
  • CDropDescription,用于存储用户定义的拖放描述(由 COleDataSourceExCOleDropTargetEx 使用)
  • CHtmlFormat,用于将 HTML 字符串转换为 HTML 剪贴板格式,反之亦然
  • CTextToRtf,用于将纯文本字符串转换为 RTF

DragDropHelper.h

通用定义和较新 SDK 头文件中的定义,以便源代码可以使用 Visual Studio 2005 及更高版本进行构建。

OleDataDemo.cpp, OleDataDemo.h

演示应用程序。

OleDataDemoDlg.cpp, OleDataDemoDlg.h

演示应用程序的主对话框。

MyListCtrl.cpp, MyListCtrl.h

CListCtrl 派生的报表样式列表

  • 剪贴板和拖放源提供纯文本、CSV 和 HTML 格式
  • 拖动时提供纯文本、CSV 和 HTML 作为虚拟文件
  • 单行纯文本的拖放目标(拖放到单个单元格)
  • 拖动经过滚动条时的自动滚动

MyEdit.cpp, MyEdit.h

基于 CEdit 的多行编辑控件

  • 拖放作为源和目标,包括控件内部的拖放
  • 接受从 Explorer 拖动过来的文件名
  • 上下文菜单,带有“选择性粘贴”子菜单(CSV、HTML 和 RTF 作为文本;文件名和文件内容)
  • 拖动经过滚动条和插入区域时的自动滚动

MyStatic.cpp, MyStatic.h

基于 CStatic 的图片控件

  • bitmap 的拖放作为源和目标
  • 从 Explorer 拖动图像文件时从文件加载图像
  • 如果源未提供 bitmap,则拖放图像
  • 拖动时将 bitmap 作为虚拟文件提供

InfoList.cpp, InfoList.h

CListCtrl 派生的报表样式列表

  • 显示有关剪贴板和拖放数据的信息
  • 数据在将新数据放入剪贴板(剪贴板查看器)或拖放到列表时更新
  • 使用用户定义的拖放描述文本和特殊光标

FileList.cpp, FileList.h

CMyListCtrl 派生类,用于以类似 Windows Explorer 的方式列出目录中的文件。

关注点

富文本控件已支持拖放,但不支持拖放图像。尝试注册 COleDropTarget 到富文本控件将失败,因为它已被注册为拖放目标。要克服这一点并使用您自己的拖放实现,必须使用 RevokeDragDrop 注销该控件作为拖放目标。

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)); 
    }
}

在 Windows Vista 及更高版本和启用的 UAC 下,无法在由具有不同特权的用户的应用程序之间拖动数据。

MSDN 中 RegisterClipboardFormat() 的描述指出:“当在剪贴板上放置或从剪贴板检索已注册的剪贴板格式时,它们必须是 HGLOBAL 值的形式。”这似乎仅适用于经典的剪贴板操作。对于 OLE 操作,即使是微软也使用带有已注册格式的 IStream。

如前所述,新样式拖放光标支持的大部分内容很少有文档记录。如果您知道本文中未提及的内容,请发表评论。

参考文献

除了 MFC OLE 类和 OLE 接口的 MSDN 文档外,以下链接可能也很有用:

历史

  • 2015 年 3 月 15 日
    • 初始版本
  • 2015 年 3 月 16 日
    • 将丢失的文件添加到源代码下载存档中
    • 对文章进行了少量修改(拼写错误,标题大写,标签,添加了有关 FileList 和图像支持的丢失信息)
  • 2016 年 4 月 18 日
    • 修复了拼写错误
    • 更改了提示框的样式
    • 添加了指向 Windows 剪贴板格式的链接
  • 2016 年 5 月 21 日
    • 添加了有关将富文本控件注册为拖放目标的信息
  • 2017 年 7 月 20 日
    • 澄清了 COleDataSource 的自动删除以及为什么默认的 SetData 失败
© . All rights reserved.