如何实现程序与浏览器之间的拖放(转)

来源:互联网 发布:百度搜索for mac 编辑:程序博客网 时间:2024/04/27 00:34

如何实现程序与浏览器之间的拖放(一)2008-12-27 13:48
http://hi.baidu.com/aoca/blog/item/aa8c602a933b6d3f5243c12f.html
翻译:How to Implement Drag and Drop Between Your Program and Explorer

介绍

最近在CodeProject留言板,看到许多关于应用程序与浏览器之间的拖放问题。如同许多Windows编程一样,了解之后就很容易实现,但是寻找答案是一件很烦人的事情。在这篇文章里,我将演示如何应用拖放。让你的应用程序与浏览器之间实现拖放文件。

例子是MFC程序,所以在这里假设你熟悉C++,MFC,并且知道使用COM对象和接口。如果你想先熟悉COM对象和接口,请参考我的文章Intro to COM。MultiFiler是一个类似“文件中转站”的小程序。你可以拖拽任意多个文件到MultiFiler,它将文件列举在列表里。你也可以将MultiFiler里的文件拖回浏览器,配合使用Shift和Ctrl键,复制或移动文件。

与浏览器的拖拽

你知道,浏览器可以让你在浏览器或桌面之间拖拽文件。当你拖拽文件时,浏览器窗口(源窗口),会创建一个实现了IDataObject接口的对象,并在其中放入一些数据。接收文件的窗口(目标窗口),使用IDataObject的函数读取这些数据;这就是为什么它知道有文件拖进来。

如果你使用ClipSpy来查看包含在IDataObject中的数据,你可以看到浏览器在数据对象中放置了这些信息:

 

其中最重要的是CF_HDROP。其他的信息是浏览器给自己使用的。如果我们将应用程序注册成拖拽目标,并且知道如何读取CF_HDROP,那么就可以接收拖拽的文件了。类似的,如果知道如何将数据放入CF_HROP,我们的应用程序就可以成为拖拽源。那么,CF_HDROP里面有些什么数据呢?请继续往下读…

DROPFILES数据结构

那么CF_HDROP具体是什么呢?它是一个DROPFILES结构体。另外有一个HDROP类型,它是一个DROPFILES结构体的指针。

DROPFILES并不复杂。它的定义如下:

struct DROPFILES
{
    DWORD pFiles;    // offset of file list
    POINT pt;        // drop point (client coords)
    BOOL fNC;        // is it on NonClient area and pt is in screen coords
    BOOL fWide;      // wide character flag
};

其中文件名列表不在结构体的定义中。它是以双null结尾的字符串列表。可是它存放在哪里呢?它紧接着fWide字段存放,pFiles保存它在内存中的位移量(相对于结构体的起始位置)。另外只有fWide在拖拽中使用,它表示文件名是ANSI还是Unicode字符。

接收来自浏览器的拖拽对象

接收拖拽对象要比创建一个简单,所以我们先接受。

在应用程序中接受拖拽对象有2种方式。第一种使用WM_DROPFILES消息,Window 3.1时候的产物。另一种是将你的应用程序注册为OLE拖拽目标。

传统的方式-WM_DROPFILES

要使用传统的方法,首先设置窗口的“accept files”样式。对于对话框,在“Extended Styles”标签里设置,如下截图:

 

你可以通过DragAcceptFiles() API在运行期设置,此方法接受2个参数。第一个是主窗口的句柄,第二个设置成TRUE,表明接受拖拽对象。如果你的窗口是CView而非对话框,你只能在运行期设置。

不管你使用哪种方式,你的窗口都会变成拖拽目标。当你从浏览器拖拽文件或文件夹到自己的窗口时,你会收到WM_DROPFILES消息。WM_DROPFILES消息的WPARAM参数是HDROP,它包含了被拖拽文件的列表。有3个API用于取出HDROP里的文件类表:DragQueryFile(), DragQueryPoint(), 和 DragFinish().

DragQueryFile()处理2件事情:返回被拖拽文件的数量,还有遍历文件列表。DragQueryPoint()返回DROPFILES结构体的pt字段。DragFinish()清理拖拽过程分配的内存。

DragQueryFile()接收4个参数:HDROP,文件名的索引,用于保存文件名的缓存,还有缓存的字符长度。如果你将-1作为索引传入,DragQueryFile()将返回文件列表的长度。否则返回文件名的字符数。将返回值与0比较,来判断函数是否执行成功。

DragQueryPoint()接收2个参数,HDROP,和用于保存DROPFILES结构体pt字段的POINT结构体指针。DragFinish()只接受一个HDROP参数。

WM_DROPFILES的处理函数如下:

void CMyDlg::OnDropFiles ( HDROP hdrop )
{
UINT uNumFiles;
TCHAR szNextFile [MAX_PATH];

    // Get the # of files being dropped.
    uNumFiles = DragQueryFile ( hdrop, -1, NULL, 0 );

    for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
        {
        // Get the next filename from the HDROP info.
        if ( DragQueryFile ( hdrop, uFile, szNextFile, MAX_PATH ) > 0 )
            {
            // ***
            // Do whatever you want with the filename in szNextFile.
            // ***
            }
        }

    // Free up memory.
    DragFinish ( hdrop );
}

如果你只是想获得文件列表,不会用到DragQueryPoint()函数(事实上,我们基本不会使用)。

新的方法-OLE拖拽目标

另一个接受拖放对象的方式,就是将你的窗口注册成OLE拖拽目标。通常情况下,使用这种方式,你必须编写一个实现IDropTarget接口的C++类。好在有MFC的COleDropTarget类减轻我们的负担。根据主窗口是CView,还是对话框,实现过程有一些不同,我将分别说明。

将CView作为拖拽目标

CView已经支持拖拽,只是没有激活。要激活它,你需要添加一个COleDropTarget成员变量,然后在视图的OnInitialUpdate()中调用Register(),使其变成拖拽目标,代码如下:

void CMyView::OnInitialUpdate()
{
    CView::OnInitialUpdate();

    // Register our view as a drop target.
    // m_droptarget is a COleDropTarget member of CMyView.
    m_droptarget.Register ( this );
}

然后你重载4个虚函数,当用户拖拽文件经过你的视图时,这些函数会被调用:

OnDragEnter():当鼠标进入你的窗口时调用。
OnDragOver():当鼠标在窗口内移动时调用。
OnDragLeave():当鼠标移除你的窗口时调用。
OnDrop():当用户把文件放入你的窗口时调用。

OnDragEnter()

OnDragEnter()是第一个被调用的函数。它的原型是:

DROPEFFECT CView::OnDragEnter( COleDataObject* pDataObject,
               DWORD dwKeyState, CPoint point );

参数分别是:

pDataObject:指向包含拖拽信息的COleDataObject对象指针。
dwKeyState:一些标志,表明哪些鼠标按键,哪些键盘按键被点击。相应的标志是MK_CONTROL, MK_SHIFT, MK_ALT, MK_LBUTTON, MK_MBUTTON, 和 MK_RBUTTON。
point:鼠标指针位置,相对视图区域的坐标。

OnDragEnter()返回DROPEFFECT值,用来告诉OLE,此拖放是否被接受,如果接受,显示哪种鼠标样式。样式值的意义是:

DROPEFFECT_NONE:不支持拖拽,鼠标变成:
DROPEFFECT_MOVE:数据将被移动到拖拽目标。鼠标变成:
DROPEFFECT_COPY:数据将被复制到拖拽目标。鼠标变成:
DROPEFFECT_LINK:数据将在拖拽目标中创建链接。鼠标变成:

通常在OnDragEnter()中验证被拖拽的数据是否是你的程序所支持的。如果不是,返回DROPEFFECT_NONE来拒绝拖拽对象。此外,根据你的意图返回相应的值。

OnDragOver()

如果你在OnDragEnter()返回的不是DROPEFFECT_NONE,鼠标进入你的窗口后,OnDragOver()函数会被调用。OnDragOver()的原型如下:

DROPEFFECT CView::OnDragOver ( COleDataObject* pDataObject,
               DWORD dwKeyState, CPoint point );

参数,返回值与OnDragEnter()一致。OnDragOver()可以让你根据鼠标所在的不同区域,按键情况返回不同的DROPEFFECT。例如,你的窗口有多个区域,显示不同信息,但是你只允许其中一个区域接受拖拽,通过检查鼠标位置的参数,当不在接受数据区域时返回DROPEFFECT_NONE。

对于组合键,通常使用如下的处理方式:

SHIFT按下(dwKeyState值为MK_SHIFT):返回DROPEFFECT_MOVE。
CONTROL按下(MK_CONTROL):返回DROPEFFECT_COPY。
同时按下(MK_SHIFT | MK_CONTROL):返回DROPEFFECT_LINK。

那些只是规范,但是最好遵守它,因为浏览器也是这样做的。如果某些操作(复制,移动,链接)在你的程序里没有意义,你不需要返回相应的DROPEFFECT。例如,在MultiFiler(我保证我会修改的)中OnDragOver()只返回DROPEFFECT_COPY。只要取保返回正确的值,这样鼠标就可以正确表现程序的操作。

OnDragLeave()

当拖拽移除你的窗口又没有放手时会被调用。它的原型是:

void CView::OnDragLeave();

它没有参数,也没有返回值,目的只是让你清理在OnDragEnter() 和 OnDragOver()中使用的资源。

OnDrop()

如果用户将对象放入你的窗口(并且你没有在最后一次的OnDragOver()返回DROPEFFECT_NONE),这是OnDrop()函数会被调用。它的原型是:

BOOL CView::OnDrop ( COleDataObject* pDataObject,
         DROPEFFECT dropEffect, CPoint point );

dropEffect参数是最后一次OnDragOver()的返回值,其它的与OnDragEnter()一致。如果操作成功(根据你自己对成功的定义)返回TRUE,否则返回FALSE。

OnDrop()是具体操作的地方-你可以根据数据和程序本来处理。在MultiFiler中,拖放的文嘉会加到窗口的列表控件中。

让对话框成为拖拽目标

如果你的主窗口是对话框(或是非派生至CView),事情会变得复杂一些。因为COleDropTarget的设计意图就是为了让CView派生的窗口支持拖放,所以你需要创建一个新类,继承COleDropTarget,并重写之前的4个函数。

派生于COleDropTarget的类,通常像这样:

class CMyDropTarget : public COleDropTarget
{
public:
    DROPEFFECT OnDragEnter ( CWnd* pWnd, COleDataObject* pDataObject,
                             DWORD dwKeyState, CPoint point );

    DROPEFFECT OnDragOver ( CWnd* pWnd, COleDataObject* pDataObject,
                            DWORD dwKeyState, CPoint point );

    BOOL OnDrop ( CWnd* pWnd, COleDataObject* pDataObject,
                  DROPEFFECT dropEffect, CPoint point );

    void OnDragLeave ( CWnd* pWnd );

    CMyDropTarget ( CMyDialog* pMainWnd );
    virtual ~CMyDropTarget();

protected:
    CMyDialog* m_pParentDlg; // initialized in constructor
};

在这个例子里,构造器中传入主窗口的指针,这样可以在拖拽函数里像主窗口发送消息,或其他的事情。你也可以按照自己的意愿修改构造器。然后按照前面的描述实现那4个函数。不同的是CWnd*参数,它是指向鼠标经过的窗口指针。

当你完成这个类的编写后,在对话框里加入此变量,然后在OnInitDialog()中调用Register()函数:

BOOL CMyDialog::OnInitDialog()
{
    // Register our dialog as a drop target.
    // m_droptarget is a CMyDropTarget member of CMyDialog.
    m_droptarget.Register ( this );
}

访问CDataObject的HDROP

如果你使用OLE拖拽对象,你的函数会收到有一个指向COleDataObject的指针。它是一个实现了IDataObject的MFC类,包含了拖拽源所创建的全部数据。通过代码先获取CF_HDROP,再得到HDROP句柄。得到HDROP后,就可以像前面说的那样使用DragQueryFile()获取拖拽文件列表。

这里是获取HDROP的代码:

BOOL CMyDropTarget::OnDrop ( CWnd* pWnd, COleDataObject* pDataObject,
                             DROPEFFECT dropEffect, CPoint point )
{
HGLOBAL hg;
HDROP   hdrop;

    // Get the HDROP data from the data object.
    hg = pDataObject->GetGlobalData ( CF_HDROP );
   
    if ( NULL == hg )
        return FALSE;

    hdrop = (HDROP) GlobalLock ( hg );

    if ( NULL == hdrop )
        {
        GlobalUnlock ( hg );
        return FALSE;
        }

    // Read in the list of files here...

    GlobalUnlock ( hg );

    // Return TRUE/FALSE to indicate success/failure
}

总结2种方式

处理 WM_DROPFILES消息:

从Windows 3.1保留而来;可能在将来被去除。
无法定制拖拽的过程,你只能在拖拽完成时收到消息。
无法看到拖拽的原始数据。
如果你不需要定制效果,这个方式比较容易代码。

使用OLE拖拽目标:

使用COM接口,比较新的方式。
有MFC的CView和COleDropTarget支持。
控制整个拖拽的过程。
通过IDataObject得到所有的数据。
需要编写更多代码,一旦你完成,这些代码可以复制粘贴到别的项目。
 

原创粉丝点击