11.3 实现拖放操作

你可以在你的应用程序中实现拖放源,拖放目标或者两者同时实现.

实现拖放源

要实现一个拖放源,也就是说要提供用户用于拖放操作的数据,你需要使用一个wxDropSource类的实例.要注意下面描述的事情都是在你的应用程序已经认定一个拖放操作已经开始以后发生的.决定拖放是否开始的逻辑,是完全需要由应用程序自己决定的,一些控件会通过产生一个拖放开始事件来通知应用程序, 在这种情况下,你不需要自己关心这一部分的逻辑(对这一部分的逻辑的关心,可能会让你的鼠标事件处理代码变得混乱).在本章中,也会提供一个何时 wxWidgets通知你拖放操作开始的大概描述.

一个拖放源需要采取的动作包括下面几步:

1 准备工作

首先,必须先创建和初始化一个将被拖动的数据对象,如下所示:

wxTextDataObject myData(wxT("This text will be dragged."));

2 开始拖动要开始拖动操作,最典型的方式是响应一个鼠标单击事件,创建一个wxDropSource对象,然后调用它的wxDropSource::DoDragDrop函数,如下所示:

wxDropSource dragSource(this);
dragSource.SetData(myData);
wxDragResult result = dragSource.DoDragDrop(wxDrag_AllowMove);

下表列出的标记,可以作为DoDragDrop函数的参数:

wxDrag_CopyOnly 只允许进行拷贝操作.
wxDrag_AllowMove 允许进行移动操作.
wxDrag_DefaultMove 默认操作是移动数据.

当创建wxDropSource对象的时候,你还可以指定发起拖动操作的窗口,并且可以选择拖动使用的光标,可选的范围包括拷贝,移动以及不能释放等.这些光标在GTK+上是图标,而在别的平台上是光标,因此你需要使用wxDROP_ICON来屏蔽这种区别,正如我们很快就将看到的拖动文本的例子中演示的那样.

3 拖动

对DoDragDrop函数的调用将会阻止应用程序进行其他处理,直到用户释放鼠标按钮(除非你重载了GiveFeedback函数以便进行其他的特殊操作)当鼠标在应用程序的一个窗口上移动时,如果这个窗口可以识别这个拖动操作协议,对应的wxDropTarget函数就会被调用,参考接下来的小节"实现一个拖放目的"

4 处理拖放结果

DoDragDrop函数返回一个拖放操作的结果,这个返回值的类型为wxDragResult,它的枚举值如下表所示:

wxDragError 在拖动操作的执行过程中出现了错误.
wxDragNone 数据源不被拖动目的接受.
wxDragCopy 数据已经被成功拷贝.
wxDragMove 数据已经被成功移动(仅适用于Windows).
wxDragLink 以完全一个链接操作.
wxDragCancel 用户已经取消了拖放操作.

你的应用程序可以针对不同的返回值进行自己的操作,如果返回值是wxDragMove,通常你需要删除绑定在数据源中的数据,然后更新屏幕显示.而如果返回值是wxDragNone,则表示拖动操作已经被取消了.下面举例说明:

switch (result)
{
    case wxDragCopy: /* 数据被拷贝或者被链接:
                          无需特别操作 */
    case wxDragLink:
        break;
    case wxDragMove: /* 数据被移动,删除原始数据 */
        DeleteMyDraggedData();
        break;
    default:         /* 操作被取消或者数据不被接受
                         或者发生了错误:
                         不做任何操作 */
        break;
}

下面的例子演示了怎样实现一个文本数据拖放源.DnDWindow包含一个m_strText成员变量,当鼠标左键按下的时候,针对 m_strText的拖放操作开始,拖放操作的结果通过一个消息框显示.另外,拖放操作将会在鼠标已经拖动了一小段距离后才会开始,因此单击鼠标动作并不会导致一个拖放操作.

void DnDWindow::OnLeftDown(wxMouseEvent& event )
{
    if ( !m_strText.IsEmpty() )
    {
        // 开始拖动操作
        wxTextDataObject textData(m_strText);
        wxDropSource source(textData, this,
                            wxDROP_ICON(dnd_copy),
                            wxDROP_ICON(dnd_move),
                            wxDROP_ICON(dnd_none));
        int flags = 0;
        if ( m_moveByDefault )
            flags |= wxDrag_DefaultMove;
        else if ( m_moveAllow )
            flags |= wxDrag_AllowMove;
        wxDragResult result = source.DoDragDrop(flags);
        const wxChar *pc;
        switch ( result )
        {
            case wxDragError:   pc = wxT("Error!");    break;
            case wxDragNone:    pc = wxT("Nothing");   break;
            case wxDragCopy:    pc = wxT("Copied");    break;
            case wxDragMove:    pc = wxT("Moved");     break;
            case wxDragCancel:  pc = wxT("Cancelled"); break;
            default:            pc = wxT("Huh?");      break;
        }
        wxMessageBox(wxString(wxT("Drag result: ")) + pc);
    }
}

实现一个拖放目的

要实现一个拖放目的,也就是说要接收用户拖动的数据,你需要使用wxWindow::SetDropTarget函数,将某个窗口和一个 wxDropTarget绑定在一起,你需要实现一个wxDropTarget的派生类,并且重载它的所有纯虚函数.另外还需要重载OnDragOver 函数,以便返回一个wxDragResult类型的返回码,以说明当鼠标指针移过这个窗口的时候,光标应该怎样显示,并且重载OnData函数来实现放置操作.你还可以通过继承wxTexTDropTarget或者wxFileDropTarget,或者重载它们的OnDropText或者 OnDropFiles函数来实现一个拖放目的.

下面的步骤将发生在拖放操作过程当中的拖放目的对象上.

1 初始化

wxWindow::SetDropTarget函数在窗口创建期间被调用,以便将其和一个拖放目的对象绑定.在窗口创建或者应用程序的其他某个部分,通过函数wxDropTarget::SetDataObject,拖放目的对象和某一种数据类型绑定,这种数据类型将用来作为拖放源和播放目的进行协商的依据.

2 拖动

当鼠标在拖放目的上以拖动的方式移动时,wxDropTarget::OnEnter,wxDropTarget:: OnDragOver和 wxDropTarget::OnLeave函数将在适当的时候被调用,它们都将返回一个对应的wxDragResult值.以便拖放操作可以对其进行合适的用户界面反馈.

3 放置

当用户释放鼠标按钮的时候,wxWidgets通过调用函数wxDataObject::GetAllFormats询问窗口绑定的 wxDropTarget对象是否接受正在拖动的数据.如果数据类型是可接受的,那么wxDropTarget::OnData将被调用.拖放对象绑定的 wxDataObject对象将进行对应的数据填充动作.wxDropTarget::OnData函数将返回一个wxDragResult类型的值,这个值将作为wxDropSource::DoDragDrop函数的返回值.

使用标准的拖放目的对象

wxWidgets提供了了标准的wxDropTarget的派生类,因此你不必在任何时候都需要实现自己的拖放对象.你只需要实现重载这些类的一个虚函数,以便在拖放的时候得到提示.

wxTextdropTarget对象可以接收被拖动的文本数据,你只需要重载OnDropText函数以便告诉wxWidgets当有文本数据被放置的时候做什么事情就可以了.下面的例子演示了当有文本数据被放置的时候,怎样将其添加列表框内.

// 一个拖放目的用来将文本填加到列表框
class DnDText : public wxTextDropTarget
{
public:
    DnDText(wxListBox *owner) { m_owner = owner; }
    virtual bool OnDropText(wxCoord x, wxCoord y,
                              const wxString& text)
    {
        m_owner->Append(text);
        return true;
    }
private:
    wxListBox *m_owner;
};
// 设置拖放目的对象
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new DnDText(listBox));

下面的例子展示了怎样使用wxFileDropTarget,这个对象可接收从资源管理器里拖动的文件对象,并且报告拖动文件的数目以及它们的名称.

// 一个拖放目的类用来将拖动的文件名添加到列表框
class DnDFile : public wxFileDropTarget
{
public:
    DnDFile(wxListBox *owner) { m_owner = owner; }
    virtual bool OnDropFiles(wxCoord x, wxCoord y,
                               const wxArrayString& filenames)
    {
        size_t nFiles = filenames.GetCount();
        wxString str;
        str.Printf( wxT("%d files dropped"), (int) nFiles);
        m_owner->Append(str);
        for ( size_t n = 0; n < nFiles; n++ ) {
            m_owner->Append(filenames[n]);
        }
        return true;
    }
private:
    wxListBox *m_owner;
};
// 设置拖放目的类
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new DnDFile(listBox));

创建一个自定义的拖放目的

现在我们来创建一个自定义的拖放目的,它可以接受URLs(网址).这一次我们需要重载OnData和 OnDragOver函数,我们还将实现一个可以被重载的虚函数OnDropURL.

// 一个自定义的拖放目的对象,可以拖放URL对象
class URLDropTarget : public wxDropTarget
{
public:
    URLDropTarget() { SetDataObject(new wxURLDataObject); }
    void OnDropURL(wxCoord x, wxCoord y, const wxString& text)
    {
        // 当然, 一个真正的应用程序在这里应该做些更有意义的事情
        wxMessageBox(text, wxT("URLDropTarget: got URL"),
                      wxICON_INFORMATION | wxOK);
    }
    // URLs不能被移动,只能被拷贝
    virtual wxDragResult OnDragOver(wxCoord x, wxCoord y,
                                      wxDragResult def)
        {
            return wxDragLink;
        }
    // 这个函数调用了OnDropURL函数,以便它的派生类可以更方便的使用
    virtual wxDragResult OnData(wxCoord x, wxCoord y,
                                  wxDragResult def)
    {
        if ( !GetData() )
            return wxDragNone;
        OnDropURL(x, y, ((wxURLDataObject *)m_dataObject)->GetURL());
        return def;
    }
};
// 设置拖放目的对象
wxListBox* listBox = new wxListBox(parent, wxID_ANY);
listBox->SetDropTarget(new URLDropTarget);

更多关于wxDataObject的知识

正如我们已经看到的那样,wxDataObject用来表示所有可以被拖放.以及可以被剪贴板操作的数据.wxDataObject最重要的特性之一,是在于它是一个"聪明"的数据块,和普通的包含一段内存缓冲,或者一些文件的哑数据不同.所谓"聪明"指的是数据对像自己可以知道它内部的数据可以支持什么数据格式,以及怎样将它的内部数据表现为那种数据格式.

所谓支持的数据格式,意思是说,这种格式可以从一个数据对象的内部数据或者将被设置的内部数据产生.通常情况下,一个数据对象可以支持多种数据格式作为输入或者输出.因此,一个数据对象可以支持从一种格式创建内部数据,并将其转换为另外一种数据格式,反之亦然.

当你需要使用某种数据对象的时候,有下面几种可选方案:

  1. 使用一种内建的数据对象.当你只需要支持文本数据,图片数据,或者是文件列表数据的时候,你可以使用wxTextdataObject,wxBitmapDataObject,或wxFileDataObject.
  2. 使用wxDataObjectSimple.从wxDataObjectSimple产生一个派生类,是实现自定义数据格式的最简便的方法,不过,这种派生类只能支持一种数据格式,因此,你将不能够使用它和别的应用程序进行交互.不过,你可以用它在你的应用程序以及你应用程序的不同拷贝之间进行数据传输.
  3. 使用wxCustomDataObject的派生类(它是wxDataObjectSimple的一个子类)来实现自定义数据对象.
  4. 使用wxDataObjectComposite.这是一个简单而强大的解决方案,它允许你支持任意多种数据格式(如果你和前面的方法结合使用的话,可以实现同时支持标准数据和自定义数据).
  5. 直接使用wxDataObject.这种方案可以实现最大的灵活度和效率,但也是最难的一种实现方案.

在拖放操作和剪贴板操作中,使用多重数据格式最简单的方法是使用wxDataObjectComposite对象,但是,这种使用方法的效率是很低的,因为每一个wxDataObjectSimple都使用自己定义的格式来保存所有的数据.试想一下,你将从剪贴板上以你自己的格式粘贴超过两百页的文本,其中包含Word,RTF,HTML,Unicode,和普通文本,虽然现在计算机的能力已经足可以应付这样的任务,但是从性能方面考虑, 你最好还是直接从wxDataObject实现一个派生类,用它来定义你的数据格式,然后在程序中指定这种类型的数据.

剪贴板操作和拖放操作,潜在的数据传输机制将在某个应用程序真正请求数据的时候,才会进行数据拷贝.因此,尽管用户可能认为在自己点击了应用程序的拷贝按钮以后数据就已存在于剪贴板了,但实际上这时候仅仅是告诉剪贴板有数据存在了.

实现wxDataObject的派生类

我们来看一下实现一个新的wxDataObject的派生类要用到哪些东西.至于怎样实现的过程,我们前面已经介绍过了,它是非常简单的.因此,我们在这里不多说了.

每一个wxDataObject的派生类,都必须重载和实现它的纯虚成员函数,那些只能用来输出数据或者保存数据(意味着只能进行单向操作)的数据对象的GetFormatCount函数在其不支持的方向上应该总是返回零.

GetAllFormats函数的参考为一个wxDataFormat类型的列表,以及一个方向(获取或设置).它将所有自己在这个方向上支持的数据格式填入这个列表.GetFormatCount函数则用来检测列表中元素的个数.

GetdataHere函数的参数是一个wxDataFormat参数,以及一个void*缓冲区.如果操作成功,返回TRue,否则返回false.这个函数必须将数据以给定的格式填入这个缓冲区,数据可以是任意的二进制数据,或者是文本数据,只要SetData函数可以识别就行了.

GetdataSize函数则返回在某种给定的数据格式下数据的大小.

GetFormatCount函数返回用于转换或者设置的当前支持数据类型的个数.

GetPreferredFormat函数则返回某个指定方向上优选的数据类型.

SetData函数的参考包括一个数据类型,一个整数格式的缓冲区大小,以及一个void*类型的缓冲区指针.你可以在适当的时候(比如将其拷贝到自己的内部结构的时候)对其进行适当的解释.这个函数在成功的时候返回TRue,失败的时候返回false.

wxWidgets的拖放操作例子

我们通过使用位于samples/dnd目录的wxWidgets的拖放操作的例子,来演示一下怎样制作一个自定义的拥有自定义数据类型的数据对象.这个例子演示了一个简单的绘图软件,可以用来绘制矩形,三角形,或者椭圆形,并且允许你对其进行编辑,拖放到一个新的位置,拷贝到剪贴板以及从新粘贴到应用程序等操作.你可以通过选择文件菜单中的新建命令来创建一个应用程序的主窗口.这个窗口的外观如下图所示:

这些图形是用继承字DnDShape的类来建模的,数据对象被称为DnDShapeDataObject.在学习怎样实现这个数据对象之前,我们先看一下应用程序是怎样使用它们的.

当一个剪贴板拷贝操作被请求的时候,一个DnDShapeDataObject对象将被增加到剪贴板,这个对象包含当前正在操作的图形的拷贝,如果剪贴板上已经有了一个对象,那么旧的对象将被释放.代码如下所示:

void DnDShapeFrame::OnCopyShape(wxCommandEvent& event)
{
    if ( m_shape )
    {
        wxClipboardLocker clipLocker;
        if ( !clipLocker )
        {
            wxLogError(wxT("Can't open the clipboard"));
            return;
        }
        wxTheClipboard->AddData(new DnDShapeDataObject(m_shape));
    }
}

剪贴板的粘贴操作也很容易理解,调用wxClipboard::GetData函数来获取位于剪贴板的图形数据对象,然后从其中获取图形数据.前面我们已经介绍过怎样在剪贴板拥有对应数据的时候允许paste菜单.shapeFormatId是一个全局变量,其中包含了自定义的数据格式名称:wxShape.

void DnDShapeFrame::OnPasteShape(wxCommandEvent& event)
{
    wxClipboardLocker clipLocker;
    if ( !clipLocker )
    {
        wxLogError(wxT("Can't open the clipboard"));
        return;
    }
    DnDShapeDataObject shapeDataObject(NULL);
    if ( wxTheClipboard->GetData(shapeDataObject) )
    {
        SetShape(shapeDataObject.GetShape());
    }
    else
    {
        wxLogStatus(wxT("No shape on the clipboard"));
    }
}
void DnDShapeFrame::OnUpdateUIPaste(wxUpdateUIEvent& event)
{
    event.Enable( wxTheClipboard->
                      IsSupported(wxDataFormat(shapeFormatId)) );
}

为了实现拖放操作,还需要一个拖放目标对象,以便在图片数据被放置的时候通知应用程序.DnDShapeDropTarget类包含一个 DnDShapeDataObject数据对象,用来扮演这个角色,当它的OnData函数被调用的时候,则表明正在放置一个图形数据对象,下面的代码演示了DnDShapeDropTarget的声明及实现:

class DnDShapeDropTarget : public wxDropTarget
{
public:
    DnDShapeDropTarget(DnDShapeFrame *frame)
        : wxDropTarget(new DnDShapeDataObject)
    {
        m_frame = frame;
    }
    // 重载基类的(纯)虚函数
    virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
    {
        m_frame->SetStatusText(_T("Mouse entered the frame"));
        return OnDragOver(x, y, def);
    }
    virtual void OnLeave()
    {
        m_frame->SetStatusText(_T("Mouse left the frame"));
    }
    virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def)
    {
        if ( !GetData() )
        {
            wxLogError(wxT("Failed to get drag and drop data"));
            return wxDragNone;
        }
        // 通知主窗口正在进行放置
        m_frame->OnDrop(x, y,
                ((DnDShapeDataObject *)GetDataObject())->GetShape());
        return def;
    }
private:
    DnDShapeFrame *m_frame;
};

在应用程序初始化函数里,主窗口被创建的时候设置这个拖放目标:

DnDShapeFrame::DnDShapeFrame(wxFrame *parent)
             : wxFrame(parent, wxID_ANY, _T("Shape Frame"))
{
    ...
    SetDropTarget(new DnDShapeDropTarget(this));
    ...
}

当鼠标左键单击的时候,拖放操作开始,其处理函数将创建一个wxDropSource对象,并且给DoDragDrop 函数传递一个DnDShapeDataObject对象,以便初始化拖放操作.DndShapeFrame::OnDrag函数如下所示:

void DnDShapeFrame::OnDrag(wxMouseEvent& event)
{
    if ( !m_shape )
    {
        event.Skip();
        return;
    }
    // 开始拖放操作
    DnDShapeDataObject shapeData(m_shape);
    wxDropSource source(shapeData, this);
    const wxChar *pc = NULL;
    switch ( source.DoDragDrop(true) )
    {
        default:
        case wxDragError:
            wxLogError(wxT("An error occured during drag and drop"));
            break;
        case wxDragNone:
            SetStatusText(_T("Nothing happened"));
            break;
        case wxDragCopy:
            pc = _T("copied");
            break;
        case wxDragMove:
            pc = _T("moved");
            if ( ms_lastDropTarget != this )
            {
                // 如果这个图形被放置在自己的窗口上
                // 不要删除它
                SetShape(NULL);
            }
            break;
        case wxDragCancel:
            SetStatusText(_T("Drag and drop operation cancelled"));
            break;
    }
    if ( pc )
    {
        SetStatusText(wxString(_T("Shape successfully ")) + pc);
    }
    //在其他情况下,状态文本已经被设置了
}

当用户释放鼠标以表明正在执行放置操作的时候,wxWidgets调用DnDShapeDropTarget::OnData函数,这个函数将以一个新的DndShape对象来调用DndShapeFrame::OnDrop函数,以便给DndShape对象设置一个新的位置.这样,拖放操作就完成了.

void DnDShapeFrame::OnDrop(wxCoord x, wxCoord y, DnDShape *shape)
{
    ms_lastDropTarget = this;
    wxPoint pt(x, y);
    wxString s;
    s.Printf(wxT("Shape dropped at (%d, %d)"), pt.x, pt.y);
    SetStatusText(s);
    shape->Move(pt);
    SetShape(shape);
}

现在,唯一剩下的事情,就是实现自定义的wxDataObject对象了.为了说明得更清楚,我们将对整个实现进行逐项说明.首先,我们来看一下自定义数据类型标识符声明,以及DndShapeDataObject类的声明,它的构造函数和析构函数,和它的数据成员.

数据类型标识符是shapeFormatId,它是一个全局变量,在整个例子中都有使用.构造函数通过GeTDataHere函数获得当前图形(如果有的话)的一个拷贝作为参数.这个拷贝也可以通过DndShape::Clone函数产生.DnDShapeDataObject的析构函数将会释放这个拷贝.

DndShapeDataObject可以提供位图和(在支持的平台上)源文件来表示它的内部数据.因此,它还拥有 wxBitmapDataObject和wxMetaFileDataObject两个类型的数据成员(以及一个标记用来指示当前正在使用哪种类型)来缓存内部数据以便在需要的时候提供这种格式.

// 自定义的数据格式标识符
static const wxChar *shapeFormatId = wxT("wxShape");

class DnDShapeDataObject : public wxDataObject
{
public:
    // 构造函数没有直接拷贝指针
    // 这样在原来的图形对象被释放以后,这里的图形对象是有效的
    DnDShapeDataObject(DnDShape *shape = (DnDShape *)NULL)
    {
        if ( shape )
        {
            // 我们需要拷贝真正的图形对象,而不是只拷贝指针
            // 这是因为图形对象有可能在任何时候被删除,在这种情况下
            // 剪贴板上的数据仍然应该是有效的
            // 因此我们使用下边的方法来实现图形拷贝
            void *buf = malloc(shape->DnDShape::GetDataSize());
            shape->GetDataHere(buf);
            m_shape = DnDShape::New(buf);
            free(buf);
        }
        else
        {
            // 不需要拷贝任何东西
            m_shape = NULL;
        }
        // 这个字符串应该用来唯一标识我们的数据格式类型
        // 除此以外,它可以是任意的字符串
        m_formatShape.SetId(shapeFormatId);
        // 我们直到需要的(也就是数据被第一次请求)时候才产生图片或者元文件数据
        m_hasBitmap = false;
        m_hasMetaFile = false;
    }
    virtual ~DnDShapeDataObject() { delete m_shape; }
    // 在这个函数被调用以后,图形数据归调用者所有
    // 调用者将负责释放相关内存
    DnDShape *GetShape()
    {
        DnDShape *shape = m_shape;
        m_shape = (DnDShape *)NULL;
        m_hasBitmap = false;
        m_hasMetaFile = false;
        return shape;
    }
    // 其他成员函数省略
    ...
    // 数据成员
private:
    wxDataFormat         m_formatShape; // 我们的自定义格式
    wxBitmapDataObject   m_dobjBitmap;  // 用来响应位图格式请求
    bool                 m_hasBitmap;   // 如果m_dobjBitmap有效为真
    wxMetaFileDataObject m_dobjMetaFile;// 用来响应元数据格式请求
    bool                 m_hasMetaFile;// 如果m_dobjMetaFile有效为真
    DnDShape             *m_shape;       // 原始数据
};

接下来我们来看一下那些用于回答和我们内部存储的数据相关的问题的函数.GetPreferredFormat只简单的返回 m_formatShape数据绑定的本地的数据格式,它是在我们的构造函数中使用wxShape类型初始化的.GetFormatCount函数用来检测某种特定的格式是否可以被用来获取或者设置数据.在获取数据的时候,只有位图和元文件格式是可以被处理的.GetDataSize函数依据请求的数据格式的不同返回合适的数据大小,如果必要的话,为了得到这个大小,你可以在这个时候创建位图成员或者元文件成员.

virtual wxDataFormat GetPreferredFormat(Direction dir) const
{
    return m_formatShape;
}
virtual size_t GetFormatCount(Direction dir) const
{
    // 我们自定义的数据格式类型即可以支持 GetData()
    // 也可以支持 SetData()
    size_t nFormats = 1;
    if ( dir == Get )
    {
        // 但是,位图格式只支持输出
        nFormats += m_dobjBitmap.GetFormatCount(dir);
        nFormats += m_dobjMetaFile.GetFormatCount(dir);
    }
    return nFormats;
}
virtual void GetAllFormats(wxDataFormat *formats, Direction dir) const
{
    formats[0] = m_formatShape;
    if ( dir == Get )
    {
        // 在获取方向上我们增加位图和元文件两种格式的支持
        //在Windows平台上
        m_dobjBitmap.GetAllFormats(&formats[1], dir);
        // 不要认为m_dobjBitmap只有一种格式
        m_dobjMetaFile.GetAllFormats(&formats[1 +
                m_dobjBitmap.GetFormatCount(dir)], dir);
    }
}
virtual size_t GetDataSize(const wxDataFormat& format) const
{
    if ( format == m_formatShape )
    {
        return m_shape->GetDataSize();
    }
    else if ( m_dobjMetaFile.IsSupported(format) )
    {
        if ( !m_hasMetaFile )
            CreateMetaFile();
        return m_dobjMetaFile.GetDataSize(format);
    }
    else
    {
        wxASSERT_MSG( m_dobjBitmap.IsSupported(format),
                       wxT("unexpected format") );
        if ( !m_hasBitmap )
            CreateBitmap();
        return m_dobjBitmap.GetDataSize();
    }
}

GetdataHere函数按照请求的数据格式类型将数据拷贝到void*类型的缓冲区:

virtual bool GetDataHere(const wxDataFormat& format, void *pBuf) const
{
    if ( format == m_formatShape )
    {
        // 使用ShapeDump结构将其转换为void*流
        m_shape->GetDataHere(pBuf);
        return true;
    }
    else if ( m_dobjMetaFile.IsSupported(format) )
    {
        if ( !m_hasMetaFile )
            CreateMetaFile();
        return m_dobjMetaFile.GetDataHere(format, pBuf);
    }
    else
    {
        wxASSERT_MSG( m_dobjBitmap.IsSupported(format),
                      wxT("unexpected format") );
        if ( !m_hasBitmap )
            CreateBitmap();
        return m_dobjBitmap.GetDataHere(pBuf);
    }
}

SetData函数只需要处理本地格式,因此,它需要做的所有事情就是使用DndShape::New函数来制作一个参数图形的拷贝:

virtual bool SetData(const wxDataFormat& format,
                       size_t len, const void *buf)
{
    wxCHECK_MSG( format == m_formatShape, false,
                  wxT( "unsupported format") );
    delete m_shape;
    m_shape = DnDShape::New(buf);
    // the shape has changed
    m_hasBitmap = false;
    m_hasMetaFile = false;
    return true;
}

实现DndShape和void*类型的互相转换的方法是非常直接的.它使用了一个ShapeDump的结构来保存图形的详细信息.下面是其实现方法:

// 静态函数用来从一个void*缓冲区中创建一个图形
DnDShape *DnDShape::New(const void *buf)
{
    const ShapeDump& dump = *(const ShapeDump *)buf;
    switch ( dump.k )
    {
        case Triangle:
            return new DnDTriangularShape(
                             wxPoint(dump.x, dump.y),
                             wxSize(dump.w, dump.h),
                             wxColour(dump.r, dump.g, dump.b));
        case Rectangle:
            return new DnDRectangularShape(
                             wxPoint(dump.x, dump.y),
                             wxSize(dump.w, dump.h),
                             wxColour(dump.r, dump.g, dump.b));
        case Ellipse:
            return new DnDEllipticShape(
                             wxPoint(dump.x, dump.y),
                             wxSize(dump.w, dump.h),
                             wxColour(dump.r, dump.g, dump.b));
        default:
            wxFAIL_MSG(wxT("invalid shape!"));
            return NULL;
    }
}
// 返回内部数据大小
size_t DndShape::GetDataSize() const
{
    return sizeof(ShapeDump);
}
// 将自己填入一个void*缓冲区
void DndShape::GetDataHere(void *buf) const
{
    ShapeDump& dump = *(ShapeDump *)buf;
    dump.x = m_pos.x;
    dump.y = m_pos.y;
    dump.w = m_size.x;
    dump.h = m_size.y;
    dump.r = m_col.Red();
    dump.g = m_col.Green();
    dump.b = m_col.Blue();
    dump.k = GetKind();
}

最后,我们回到DnDShapeDataObject数据对象,下边的这些函数用来在需要的时候将内部数据转换为位图或者元数据:

void DnDShapeDataObject::CreateMetaFile() const
{
    wxPoint pos = m_shape->GetPosition();
    wxSize size = m_shape->GetSize();
    wxMetaFileDC dcMF(wxEmptyString, pos.x + size.x, pos.y + size.y);
    m_shape->Draw(dcMF);
    wxMetafile *mf = dcMF.Close();
    DnDShapeDataObject *self = (DnDShapeDataObject *)this;
    self->m_dobjMetaFile.SetMetafile(*mf);
    self->m_hasMetaFile = true;
    delete mf;
}
void DnDShapeDataObject::CreateBitmap() const
{
    wxPoint pos = m_shape->GetPosition();
    wxSize size = m_shape->GetSize();
    int x = pos.x + size.x,
        y = pos.y + size.y;
    wxBitmap bitmap(x, y);
    wxMemoryDC dc;
    dc.SelectObject(bitmap);
    dc.SetBrush(wxBrush(wxT("white"), wxSOLID));
    dc.Clear();
    m_shape->Draw(dc);
    dc.SelectObject(wxNullBitmap);
    DnDShapeDataObject *self = (DnDShapeDataObject *)this;
    self->m_dobjBitmap.SetBitmap(bitmap);
    self->m_hasBitmap = true;
}

我们自定义的数据对象的实现到此为止就全部完成了,部分细节(比如图形怎样把自己绘制到用户界面上)没有在此列出,你可以参考wxWidgets自带的samples/dnd中的代码.

wxWidgets中的拖放相关的一些帮助

下面我们来描述一些在实现拖放操作时可以给你帮助的控件.

wxTreeCtrl

你可以使用EVT_TREE_BEGIN_DRAG或EVT_TREE_BEGIN_RDRAG事件映射宏来增加对鼠标左键或右键开始的拖放操作的处理,这是这个控件内部的鼠标事件处理函数实现的.在你的事件处理函数中,你可调用wxtreeEvent::Allow来允许 wxtreeCtrl使用它自己的拖放实现来发送一个EVT_TREE_END_DRAG事件.如果你选择了使用tree控件自己的拖放代码,那么随着拖放鼠标指针的移动,将会有一个小的拖动图片被创建,并随之移动,整个放置的操作则完全需要在应用程序的结束放置事件处理函数中实现.

下面的例子演示了怎样使用树状控件提供的拖放事件,来实现当用户把树状控件中的一个子项拖到另外一个子项上的时候,产生一个被拖动子项的拷贝.

BEGIN_EVENT_TABLE(MyTreeCtrl, wxTreeCtrl)
    EVT_TREE_BEGIN_DRAG(TreeTest_Ctrl, MyTreeCtrl::OnBeginDrag)
    EVT_TREE_END_DRAG(TreeTest_Ctrl, MyTreeCtrl::OnEndDrag)
END_EVENT_TABLE()
void MyTreeCtrl::OnBeginDrag(wxTreeEvent& event)
{
    // 需要显式的指明允许拖动
    if ( event.GetItem() != GetRootItem() )
    {
        m_draggedItem = event.GetItem();
        wxLogMessage(wxT("OnBeginDrag: started dragging %s"),
                      GetItemText(m_draggedItem).c_str());
        event.Allow();
    }
    else
    {
        wxLogMessage(wxT("OnBeginDrag: this item can't be dragged."));
    }
}
void MyTreeCtrl::OnEndDrag(wxTreeEvent& event)
{
    wxTreeItemId itemSrc = m_draggedItem,
                  itemDst = event.GetItem();
    m_draggedItem = (wxTreeItemId)0l;
    // 在哪里拷贝这个子项呢?
    if ( itemDst.IsOk() && !ItemHasChildren(itemDst) )
    {
        // 这种情况下拷贝到它的父项内
        itemDst = GetItemParent(itemDst);
    }
    if ( !itemDst.IsOk() )
    {
        wxLogMessage(wxT("OnEndDrag: can't drop here."));
        return;
    }
    wxString text = GetItemText(itemSrc);
    wxLogMessage(wxT("OnEndDrag: '%s' copied to '%s'."),
                  text.c_str(), GetItemText(itemDst).c_str());
    //  增加新的子项
    int image = wxGetApp().ShowImages() ? TreeCtrlIcon_File : -1;
    AppendItem(itemDst, text, image);
}

如果你想自己处理拖放操作,比如使用wxDropSource来实现,你可以在拖放开始事件处理函数中使用wxtreeEvent:: Allow函数来禁止默认的拖放动作,并且开始你自己的拖放动作.这种情况下拖放结束事件将不会被发送,因为你已经决定用自己的方式来处理拖放(如果使用 wxDropSource::DoDragDrop函数,你需要自己检测何时拖放结束).

wxListCtrl

这个类没有提供默认的拖动图片,或者拖放结束事件,但是,它可以让你知道什么时候开始一个拖放操作.使用 EVT_LIST_BEGIN_DRAG或EVT_LIST_BEGIN_RDRAG事件映射宏来实现你自己的拖放代码.你也可以使用 EVT_LIST_COL_BEGIN_DRAG,EVT_LIST_COL_DRAGGING和EVT_LIST_COL_END_DRAG来检测何时某一个单独的列正在被拖动.

wxDragImage

在你实现自己的拖放操作的时候,可以很方便地使用wxDragImage类.它可以在顶层窗口上绘制一副图片,还可以移动这个图片,并且不损坏它后面的窗口.这通常是通过在移动之前保存一份背景窗口,并且在需要的时候,重绘背景窗口来实现的.

下图演示了wxDragImage例子中的主窗口,你可以在wxWidgets的samples/dragimag中找到这个例子.当主窗口上的三个拼图块被拖动时,将会采用不同的拖动图片,分别为图片本身,一个图标,或者一个动态产生的包含一串文本的图片.如果你选择使用整个屏幕这个选项,那么这个图片可以被拖动到窗口以外的地方,在Windows平台上,这个例子即可以使用标准的wxDragImage的实现(默认情形)来编译,也可以使用本地原生控件来编译,后者需要你在dragimag.cpp中将wxUSE_GENERIC_DRAGIMAGE置为1.

当检测到开始拖动操作的时候,创建一个wxDragImage对象,并且把它存放在任何你可以在整个拖动过程中访问的地方,然后调用 BeginDrag来开始拖动,调用EndDrag来结束拖动.要移动这个图片,第一次要使用Show函数,后面则需要使用Move函数.如果你需要在拖动过程当中刷新屏幕内容(比如在dragimag的例子中高量显示某个项目),你需要先调用Hide函数,然后更新你的窗口,然后调用Move函数,然后调用Show函数.

你可以在一个窗口内拖动,也可以在整个屏幕或者屏幕的任何一部分内拖动,以节省资源.如果你希望用户可以在两个拥有不同的顶层父窗口的窗口之间拖动,你就必须使用全屏拖动的方式.全屏拖动的效果不一定完美,因为它在开始拖动的时候获取了一副整个屏幕的快照,屏幕后续的改动则不会进行相应的更新.如果在你的拖动过程当中,别的应用程序对屏幕内容进行了改动,将会影响到拖动的效果.

在接下来的例子中,基于上面的那个例子,MyCanvas窗口显示了很多DragShap类的图片,它们中的每一个都和一副图片绑定. 当针对某个DragShap的拖动操作开始时,一个使用其绑定的图片的wxDragImage对象被创建,并且BeginDrag被调用.当检测到鼠标移动的时候,调用wxDragImage::Move函数来移动来将这个对象进行相应的移动.最后,当鼠标左键被释放的时候,用于指示拖动的图片被释放,被拖动的图片则在其新的位置被重绘.

void MyCanvas::OnMouseEvent(wxMouseEvent& event)
{
    if (event.LeftDown())
    {
        DragShape* shape = FindShape(event.GetPosition());
        if (shape)
        {
            // 我们姑且认为拖动操作已经开始
            // 不过最好等待鼠标移动一段时间再真正开始.
            m_dragMode = TEST_DRAG_START;
            m_dragStartPos = event.GetPosition();
            m_draggedShape = shape;
        }
    }
    else if (event.LeftUp() && m_dragMode != TEST_DRAG_NONE)
    {
        // 拖动操作结束
        m_dragMode = TEST_DRAG_NONE;
        if (!m_draggedShape || !m_dragImage)
            return;
        m_draggedShape->SetPosition(m_draggedShape->GetPosition()
                            + event.GetPosition() - m_dragStartPos);
        m_dragImage->Hide();
        m_dragImage->EndDrag();
        delete m_dragImage;
        m_dragImage = NULL;
        m_draggedShape->SetShow(true);
        m_draggedShape->Draw(dc);
        m_draggedShape = NULL;
    }
    else if (event.Dragging() && m_dragMode != TEST_DRAG_NONE)
    {
        if (m_dragMode == TEST_DRAG_START)
        {
            // 我们将在鼠标已经移动了一小段距离以后开始真正的拖动
            int tolerance = 2;
            int dx = abs(event.GetPosition().x - m_dragStartPos.x);
            int dy = abs(event.GetPosition().y - m_dragStartPos.y);
            if (dx <= tolerance && dy <= tolerance)
                return;
            // 开始拖动.
            m_dragMode = TEST_DRAG_DRAGGING;
            if (m_dragImage)
                delete m_dragImage;
            // 从画布上清除拖动图片
            m_draggedShape->SetShow(false);
            wxClientDC dc(this);
            EraseShape(m_draggedShape, dc);
            DrawShapes(dc);
            m_dragImage = new wxDragImage(
                                           m_draggedShape->GetBitmap());
            // 被拖动图片的左上角到目前位置的偏移量
            wxPoint beginDragHotSpot = m_dragStartPos
                                         m_draggedShape->GetPosition();
            // 总认为坐标系为被捕获窗口的客户区坐标系
            if (!m_dragImage->BeginDrag(beginDragHotSpot, this))
            {
                delete m_dragImage;
                m_dragImage = NULL;
                m_dragMode = TEST_DRAG_NONE;
            } else
            {
                m_dragImage->Move(event.GetPosition());
                m_dragImage->Show();
            }
        }
        else if (m_dragMode == TEST_DRAG_DRAGGING)
        {
            // 移动这个图片
            m_dragImage->Move(event.GetPosition());
        }
    }
}

如果你希望自己绘制用于拖动的图片而不是使用一个位图,你可以实现一个wxGenericDragImage的派生类,重载其 wxDragImage::DoDrawImage函数和wxDragImage::GetImageRect函数.在非windows的平台上, wxDragImage是wxGenericDragImage的一个别名而已,而windows平台上实现的wxDragImage不支持 DoDrawImage函数,也限制只能绘制有时候显得有点恐怖的半透明图片,因此,你可以考虑在所有的平台上都使用 wxGenericDragImage类.

当你开始拖动操作的时候,就在正准备调用wxDragImage::Show函数之前,通常你需要现在屏幕上擦除你要拖动的对象,这可以使得 wxDragImage保存的背景中没有正在拖动的对象,因此整个的拖动过程看上去也更合理,不过这将导致屏幕在开始拖动的时候会有一点点的闪烁.要避免这种闪烁(仅适用于使用wxGenericDragImage的情况),你可以重载wxGenericDragImage的 UpdateBackingFromWindow函数,使用传递给你的设备上下文绘制一个不包含正在拖动对象的背景,然后你就不需要在调用 wxDragImage::Show函数之前擦除你要拖动的对象了,整个拖动过程的屏幕就将会是平滑而无闪烁的了.