CObjects 的拖放管理器






4.58/5 (8投票s)
2000年1月17日

161782

3929
MFC CObject 派生类的拖放/剪贴板管理器类

引言
有许多 Windows C++ 编程书籍都包含关于如何使用剪贴板和拖放的章节。有些演示如何使用基本的 Windows API 来执行这些操作,有些使用 MFC,还有些使用 COM/OLE 来完成工作。
但是,当我开始想自己进行剪贴板操作时,我发现所有这些来源都有一个共同的限制:它们通常只处理纯文本 (CF_TEXT) 传输,而我需要处理我自己的特殊对象。
然后,我读了 Keith Rule 的一篇文章,其中演示了使用 CMemFile 和 CSharedFile 对象通过序列化来传输文本块。我决定可以将这个过程扩展到适用于任何从 CObject 派生的类,只要该类有自己的 Serialize() 方法。
结果就是本文介绍的拖放管理器。
目标读者
本文档面向希望在文档-视图架构应用程序中实现拖放操作的其他 MFC 程序员。假设您基本熟悉文档-视图架构。使用此处描述的类不需要了解 OLE 或 COM。
致谢
- 使用序列化来处理缓冲区的输入和输出数据的想法来自于 Keith Rule 的文章《Basic Copy/Paste and Drag/Drop Support》(基本的复制/粘贴和拖放支持)
- 拖放事件处理的通用策略来自 David S. Platt 的《The Essence of OLE with Active X》(OLE 与 Active X 的精髓)第一章。
拖放管理器类 - CDragDropMgr
要求
我计划在一个标准的 SDI 图形绘图程序中使用这个类。它需要
- 在剪切或复制时将图形对象(通常是形状和线条)复制到剪贴板。
- 将这些对象粘贴回视图窗口。
- 在视图中拖动选定的对象到新位置。
- 将对象拖到另一个视图窗口并将其放置到位。
- 除了应用程序中使用的特殊图形对象外,还需要允许用户从其他应用程序选择文本块,然后通过剪贴板将它们复制/粘贴到图形程序中,或者直接将它们拖到视图中。
用户不需要能够将图形对象转换为纯文本以粘贴或拖放到其他应用程序中。
环境
管理器类设计用于由视图类创建和使用,视图类通常派生自 CView 或 CScrollView。在 MDI 类型应用程序中,每个视图创建自己的管理器对象。
由其拥有视图负责将更改传达给控制它的 CDocument 类,以及传达给可能受更改影响的任何其他视图。例如,如果某个视图接受了“正方形”对象的拖放,它必须告知其拥有文档添加新形状,然后告知其他视图更新自身以显示新对象。
管理器类使用 OLE 和 MFC。它使用 Visual Studio 5.0 在 Windows NT 4.0 和 Windows 95 上编译和测试。
继承
管理器类不派生自任何其他类。
ddmgr.h 中的静态或全局变量
static BOOL g_bOleInitCalled = FALSE;
此变量用于指示此管理器或同一应用程序中的其他管理器是否已初始化 OLE。
项目说明
编译或链接编辑不需要特殊的项目设置。在警告级别 3 下成功编译。
公共方法
构造函数
CDragDropMgr(BOOL bInitOle = TRUE);
构造函数通过调用 InitOle() 来设置 OLE 环境,如果调用者指示需要这样做的话。父应用程序本身不必初始化 OLE。如果已经初始化,则应使用标志 FALSE 调用构造函数,或在创建任何管理器对象之前将 g_bOleInitCalled 标志设置为 TRUE。
当 OLE 已被初始化时尝试初始化 OLE 会有一个恼人的调试断言失败。此函数旨在尽量减少其影响。
MakeDropTarget
void MakeDropTarget(CView* pView);
调用者使用此函数将自身注册为一个有效的 OLE 下降目标。
AddFormat
BOOL AddFormat(CString csFormat);
调用者使用此函数创建管理器对象应识别的对象格式列表。
如果 csFormat 设置为“CF_TEXT”(请注意,这是一个 string,而不是 UINT CF_TEXT 格式值),则可以接受 CF_TEXT 类型的数据(但不能复制出来)。
OkToPaste
BOOL OkToPaste();
如果剪贴板上有可用的识别数据类型,此方法返回 TRUE。它不用于拖放操作。它实际上只是下面描述的 AvailableDataType() 方法的简写形式。
AvailableDataType
CString AvailableDataType(COleDataObject* pDataObject);
此方法返回调用者引用的 OLE 数据对象中包含的数据类型的 string 版本。如果 pDataObject 为 NULL,则该方法假定调用者想检查剪贴板缓冲区。
该方法返回一个包含数据类型的 string,或者在数据未被管理器识别时返回一个空 string。
PrepareDrop
BOOL PrepareDrop(BOOL        bToClipboard,
                 LPCTSTR     lpstrFormat,
                 CObject*    pObj,
                 DROPEFFECT* pDropEffect);
此函数用于将 CObject 派生对象复制到剪贴板或 OLE 数据对象以进行拖动。
调用者传入
- bToClipboard:一个标志,指示目标对象是发送到剪贴板还是准备进行拖放
- lpstrFormat:要使用的格式类型(指向字符- string的指针)
- pObj:指向目标对象的指针,该对象必须有自己的- Serialize()方法。
- pDropEffect:指向- DROPEFFECT枚举的指针,OLE 使用该枚举。调用者可以将此设置为- NULL(如果复制到剪贴板),但如果准备进行拖放,则不能为- NULL。
通常从以下位置调用此函数
- 视图类的 OnLButtonDown()事件处理程序,在开始拖放时。
- 视图(或文档)类的 OnEditCopy()和/或OnEditCut()事件处理程序,在复制数据到剪贴板或删除对象时。
DoDrop
BOOL DoDrop(CObject*        pO,
            COleDataObject* pDataObject,
            LPCTSTR         lpstrFormat);
此方法是 PrepareDrop() 的逆操作。它将数据从剪贴板或拖放缓冲区复制到目标对象以供调用者使用。
调用者负责创建目标对象并确保其拥有自己的 Serialize() 方法版本。
调用者传入
- pO:指向要填充数据的对象的指针,
- pDataObject:指向包含数据的 OLE 数据对象的指针。如果此指针为- NULL,则表示数据应来自剪贴板。
- lpstrFormat:指向数据格式名称的指针。
通常从以下位置调用此函数
- 视图类的 OnDrop()事件处理程序,在拖放操作结束时,用于加载数据以供使用。
- 视图(或文档)类的 OnEditPaste()方法,在从剪贴板进行粘贴时。
OnDragOver
DROPEFFECT OnDragOver(COleDataObject* pDataObject,
                      DWORD           dwKeyState,
                      CPoint          point);
此方法从调用者的 OnDragOver() 事件处理程序中调用。
它返回正在发生的拖动的类型
如果用户只是拖动一个有效对象,它返回 DROPEFFECT_MOVE。
如果用户按住 Control 键拖动一个有效对象,它返回 DROPEFFECT_COPY。如果需要,调用者可以在放置位置创建一个对象的副本,而不是移动对象。
调用者只需传入其自身的 OnDragOver() 事件处理程序接收到的参数。
OnDragEnter
BOOL OnDragEnter(COleDataObject* pDataObject);
从视图类的 OnDragEnter() 事件处理程序中调用此方法。
如果传入的 OLE 数据对象包含管理器识别的数据,则返回 TRUE,否则返回 FALSE。
GetCFText
BOOL GetCFText(CStringArray*   pcsText,
               COleDataObject* pDataObject);
这是一个专门的方法,用于代替 DoDrop() 来处理传入的 CF_TEXT 数据。
该方法将传入的 CStringArray 填充为数据缓冲区中包含的文本行。如果 pDataObject 为 NULL,则表示数据将从剪贴板缓冲区中获取。
Protected Methods
InitOle()
BOOL InitOle();
此方法为您的应用程序设置 OLE 环境。它从管理器的构造函数中调用。
公共属性
无。
受保护属性
BOOL m_bOkToDrop;
用于指示存在一个“合法”对象可以放置。
COleDropTarget m_DropTarget;
用于注册“拥有”视图
CStringArray m_csFormats;
用于存储拥有视图能够处理的已注册剪贴板数据格式的文本 string 名称。
CArray<UINT,UINT> m_nFormats;
与已注册格式字符串关联的 UINT 值的并行数组。
在示例应用程序“TextDemo”中使用管理器
上述描述可能不足以让您实际使用该类。为此,一个示例应用程序可能是最好的说明。
TextDemo App
textdemo 应用程序演示了如何使用拖放管理器类。它还演示了一个基本的文本块对象录入类的使用。这个类将是另一篇文章的主题,因此此处不作详细解释。
textdemo 应用程序仅作为演示拖放管理器和基本文本块对象的框架。因此它非常简单,用户界面也比较粗糙。
Textdemo 还执行了许多与拖放对象无关的后台操作:它必须跟踪对象,在适当时候重绘它们,处理对象选择等。本文档不涵盖任何这些主题。您可能不喜欢我实现这些任务逻辑的方式;希望您能认识到您可以按照自己的意愿设计应用程序,同时仍然使用拖放管理器类。
Textdemo 允许您通过在屏幕上键入(**draw|text blocks** 菜单选项)来创建文本块,通过在屏幕上单击(**draw|squares** 菜单选项)来创建灰色正方形,或通过进入“选择模式”(**draw|select objects** 菜单选项)来选择对象。
Textdemo 使用两种不同的对象类型,以展示如何使用拖放管理器处理多种对象类型。
当您选择了一个对象后,您可以将其复制到剪贴板,粘贴到视图中(它出现在左上角),或者删除它(它会被剪切到剪贴板)。
如果您拖动一个选定的对象,您可以将其移动到视图中的新位置,或者将其拖到桌面上的另一个 textdemo 实例。
您还可以选择另一个应用程序中的纯文本块,将其复制到剪贴板,然后粘贴到 textdemo 中(它会变成一个文本块对象),或者将选定的文本直接拖到 textdemo 中并放置在所需位置。
如果您在拖动时按住 Control 键,会创建一个选定对象的副本。
下面的部分详细介绍了为了使用拖放管理器类需要创建的视图类方法。我将这些函数收集到视图类中仅仅是为了方便。没有理由不能在文档类中处理其中一些函数,如果这对您来说更合适的话。
Textdemo 使用了一些定义,在 globals.h 中
#define MY_TXTBLK "CTxtBlk"
#define MY_SQUARE "CSquare"
#define PLAIN_TXT "CF_TEXT" //Special for ddmgr class
准备和释放管理器
在视图类的构造函数中,为此视图创建管理器对象,并为其提供合法对象类型的列表
CTextDemoView::CTextDemoView()
{
    . . .
    m_pDDMgr =  
    new CDragDropMgr; m_pDDMgr->AddFormat(MY_TXTBLK);
    //Seeglobals.h
    m_pDDMgr->AddFormat(MY_SQUARE);m_pDDMgr->AddFormat(PLAIN_TXT);
    
    . . .
}
在析构函数中,释放管理器对象
CTextDemoView::~CTextDemoView()
{
    if (m_pDDMgr != NULL)
        delete m_pDDMgr;
}
消息(事件)处理程序
OnCreate
在视图类的 OnCreate() 事件处理程序中,将此视图注册为下降目标。这必须在此处完成,而不是在构造函数中,因为视图在此之前本身无效。
int CTextDemoView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    . . .
    
    if (m_pDDMgr == 
        NULL) return
        
    -1;m_pDDMgr->MakeDropTarget((CView*)this);
    
    . . .
}
OnLButtonDown
如果当前操作是“选择对象”,请检查是否已选择对象。如果已选择对象,请始终通过 PrepareDrop() 将其数据复制到拖放缓冲区。如果管理器指示正在移动选定的对象,则从应用程序中删除该对象(其数据在 OLE 缓冲区中是安全的)。
void CTextDemoView::OnLButtonDown(UINT nFlags, CPoint point)
{ 
    . . . 
    //Start a select action. 
    if(pDoc->CurrentAction() ==  ACTION_SELECT)
    {
        m_pTextList->HideCaret(this);
        StartSelectAction(point);
    }
    . . .
}
void CTextDemoView::StartSelectAction(CPoint point)
{
    CTextDemoDoc* pDoc = GetDocument();
    FindSelection(point);
    if (pDoc->AnObjectIsSelected())
    {
        DROPEFFECT de = DROPEFFECT_NONE;
        if (m_pDDMgr->PrepareDrop(DO_DRAGDROP, //globals.h
                                  DDFormat(),
                                  pDoc->SelObjectPtr(),
                                  &de))
        {
            pDoc->CheckAndSetCursor();
            SetMoving(de == DROPEFFECT_MOVE);
            if (MovingAnObject())
                pDoc->DeleteSelectedObject();
        }
    }
}
OnDragOver
在拖动操作期间,此事件在鼠标移动时触发。只需调用管理器对象的 OnDragOver() 方法即可让其处理事务。该方法返回正在进行的拖动类型(移动或复制)。
DROPEFFECT CTextDemoView::OnDragOver(
           COleDataObject* pDataObject,
           DWORD           dwKeyState,
           CPoint          point) 
{
    DROPEFFECT de = m_pDDMgr->OnDragOver(pDataObject,
                         dwKeyState,
                         point);
    SetMoving(de == DROPEFFECT_MOVE);
    return de;
}
OnDrop
当用户将数据放入视图时,此事件触发。
事件代码仅确定了数据类型,如果合法,则执行逻辑以
- 创建对象的新实例
- 通过 DoDrop()使用缓冲区中的数据加载它
- 将新对象放置到视图中的正确位置
BOOL CTextDemoView::OnDrop(COleDataObject* pDataObject,
                           DROPEFFECT      dropEffect,
                           CPoint point) 
{
    CString csF = m_pDDMgr->AvailableDataType(pDataObject);
    if (csF == MY_TXTBLK)
        return DropTextBlock(pDataObject,dropEffect,point);
    else if (csF == MY_SQUARE)
        return DropSquare(pDataObject,dropEffect,point);
    else if (csF == PLAIN_TXT)
        return DropCFText(pDataObject,dropEffect,point);
    return FALSE;
}
此处仅显示 DropSquare 的代码;其他两个函数使用基本相同的逻辑。
BOOL CTextDemoView::DropSquare(
                    COleDataObject* pDataObject,
                    DROPEFFECT      dropEffect,
                    CPoint point) 
{
    CClientDC dc(this);
    CTextDemoDoc* pDoc = GetDocument();
    OnPrepareDC(&dc);
    dc.DPtoLP(&point);
    //Create a new CTxtBlk object
    CSquare* pS = new CSquare(CPoint(0,0));
    if (pS == NULL)
        return FALSE;
    pDoc->SetModifiedFlag(TRUE);
    //If created OK, load the drop buffer contents into
    //the new text block, and place it
    //at the point of the drop.
    if (!m_pDDMgr->DoDrop(pS,
                          pDataObject,
                          MY_SQUARE))
    {
        AfxMessageBox("Drop failed");
        //Delete the newly-created pS above:
        //It will be on the tail of the list
        pDoc->DeleteLastSquare();
        return FALSE;
    }
    //Set new object to drop location
    pS->SetLocationTo(point);
    pDoc->m_SquList.AddTail(pS);
    if (dropEffect == DROPEFFECT_MOVE)
        SetMoving(FALSE);
    RedrawArea(pS->BoundingRect());
    return TRUE;
}
OnDragEnter
当用户首次将对象拖入视图时,此事件触发。
只需调用管理器的 OnDragEnter() 方法即可让其进行设置。
DROPEFFECT CTextDemoView::OnDragEnter(
           COleDataObject* pDataObject,
           DWORD           dwKeyState,
           CPoint          point) 
{
    m_pDDMgr->OnDragEnter(pDataObject);
    return this->OnDragOver(pDataObject,dwKeyState,point);
}
OnDragLeave
此处理程序不需要任何操作。
OnEditCopy, OnEditCut
当用户复制或删除对象时,这些事件会触发(您自己对这些操作的事件处理程序可能有所不同)。在这两种情况下,都将选定的对象复制到剪贴板。
void CTextDemoView::OnEditCopy()
{
   CopySelectedObjectToClipboard();
}
void CTextDemoView::OnEditCut()
{
    CopySelectedObjectToClipboard();GetDocument()->DeleteSelectedObject();
}
void CTextDemoView::CopySelectedObjectToClipboard() 
{
    CTextDemoDoc* pDoc = 
    GetDocument(); if(!pDoc->AnObjectIsSelected())
        return;
    DROPEFFECT drop = 
     DROPEFFECT_NONE;
     m_pDDMgr->PrepareDrop(DO_CLIPBOARD,
                 DDFormat(), pDoc->SelObjectPtr(),
                 &drop);
}
OnUpdateEditPaste
如果管理器指示它具有可用于粘贴的有效数据,此事件将启用“粘贴”编辑菜单选项。
void CTextDemoView::OnUpdateEditPaste(CCmdUI*pCmdUI)
{
	pCmdUI->Enable(m_pDDMgr->OkToPaste()
	                  &&!GetDocument()->UserIsDrawingText());
}
OnEditPaste
当用户将数据从剪贴板粘贴到视图时,此事件触发。
void CTextDemoView::OnEditPaste() 
{
    CTextDemoDoc* pDoc = GetDocument();
    CString csF = m_pDDMgr->AvailableDataType(NULL);
    if (csF == MY_TXTBLK)
        DropTextBlock(NULL,DROPEFFECT_NONE,CPoint(0,0));
    else if (csF == MY_SQUARE)
    {
        //Cheating just a little here. . .
        CPoint pt(SQ_HALFSIDE,SQ_HALFSIDE);
        GetDC()->LPtoDP(&pt);
        DropSquare(NULL,DROPEFFECT_NONE,pt);
    }
    else if (csF == PLAIN_TXT)
        DropCFText(NULL,DROPEFFECT_NONE,CPoint(0,0));
    DeselectCurrentObject();
}
限制
- 当从缓冲区进行序列化时,剪贴板和 OLE 数据对象似乎不识别版本化的架构。如果您查看 CSquare类中的代码,您会发现我通过使用自己的相对粗糙的架构方法来解决这个问题。
- 该管理器不允许导入超过 1000 个字符的 CF_TEXT数据。当然,您可以根据需要更改此设置。
- 该管理器没有提供创建 CF_TEXT数据的方法。但是,扩展管理器来处理这种情况应该不难:您只需要编写一个“PrepareDrop”方法将数据以CF_TEXT格式输出。数据如何进入类是真正需要考虑的问题。这将取决于您的父应用程序在做什么。
结论
我希望您觉得这个类很有趣,并且您将能够(或修改它以适应您自己的需求)在您可能开发的任何绘图程序中使用它。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。
