使用 OLE 拖放列表框项






4.36/5 (8投票s)
使用 OLE 拖放重新排列列表框项。
引言
OLE 使拖放操作变得轻而易举。它允许两个不相关的应用程序通过剪贴板格式以它们都能理解的格式交换数据。使用 OLE 进行拖放是一个相对简单的任务。使用 MFC,实际上要做的不多,只需创建适当的对象并调用它们的适当方法。有三个 MFC 类参与拖放操作。这些类是 COleDataSource
、COleDropSource
和 COleDropTarget
。
背景
以下是对每个类功能的简要解释。COleDataSource
是持有将在源到目标之间传输的数据的类。COleDropSource
是一个相对较小的类,可在拖放操作期间提供视觉反馈。最后,COleDropTarget
是处理目标端所有事情的类。实际上,大多数人只会使用 COleDataSource
和 COleDropTarget
,因为大多数人不需要更改 COleDropSource
的行为,而且它是由 COleDataSource
自动创建的。
拖放操作包括源对象创建一个 COleDataSource
,向其附加一些数据,并调用 COleDataSource
对象的 DoDragDrop
方法。目标对象需要实现一个 COleDropTarget
类。COleDropTarget
类有一些在操作期间会被调用的虚拟函数:OnDragEnter
、OnDragOver
、OnDragLeave
和 OnDrop
是最重要的。必须重写这些方法,以便我们能够告诉系统该做什么。
让我们看一分钟源端。在这里,我们将创建一个 COleDataSource
并向其附加一些文本。
void CMyDragAndDropWnd::StartDrag() { //create the COleDataSource, and attach the data to it COleDataSource DataSource; //create a chunck of memory that will hold "This is a test" 15 chars HGLOBAL hData = GlobalAlloc(GMEM_MOVEABLE,15); char *pChar = (char *)GlobalLock(hData); strcpy(pChar,"This is a test"); GlobalUnlock(hData); if (hData) { //attach the data to the COleDataSource Object DataSource.CacheGlobalData(CF_TEXT,hData); //allow the user to drag it. DROPEFFECT DropEffect = DataSource.DoDragDrop(GetDragItemEffects(m_DraggedIndex)); //Once DoDragDrop returns we can check the return value stored in DropEffect //to see what kind of dropping happened. Like move, copy or shortcut } }
“放置”端的事情比拖动部分要复杂一些。我们必须继承自 COleDropTarget
并重写一些方法。希望接收放置的窗口需要有一个继承类的对象,并使用 Register
方法向其注册。让我们看看继承自 COleDropTarget
的简单类是什么样的。
//this method is called when the Drag operation first enters the window //attached to this COleDropTarget object DROPEFFECT CMyDropTarget::OnDragEnter(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState,CPoint point) { //if the data is the kind we want if (pDataObject->IsDataAvailable(CF_TEXT)) { //here we can take bring window to top //since what is attached to the COleDropTarget object is //most likely a child window, you would probably have to //send it a message so that it can activate it's parent //or something. SendMessage(m_hWnd,...,...,...); //we can handle copy and move of this data return DROPEFFECT_COPY|DROPEFFECT_MOVE; } //we can't handle this type of data return DROPEFFECT_NONE; } DROPEFFECT CMyDropTarget::OnDragOver(CWnd* pWnd, COleDataObject* pDataObject, DWORD dwKeyState,CPoint point) { //if the data is the kind we want if (pDataObject->IsDataAvailable(CF_TEXT)) { //we can handle copy and move of this data return DROPEFFECT_COPY|DROPEFFECT_MOVE; } //we can't handle this type of data return DROPEFFECT_NONE; } void CMyDropTarget::OnDragLeave(CWnd* pWnd) { //we can use this method to do any kind of clean up //in this case we don't have anything } BOOL CMyDropTarget::OnDrop(CWnd* pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect,CPoint point) { BOOL Ret = FALSE; //if it is a format that we handle if (pDataObject->IsDataAvailable(CF_TEXT)) { //grab the data HGLOBAL hGlobal = pDataObject->GetGlobalData(m_cfFormat); //Do something with the data //here you would normally send a message back to the //window registered to the COleDropTarget to tell it //to do something with the data SendMessage(m_hWnd,WM_DOSOMETHING,hGlobal,0); Ret = TRUE; } return Ret; }
就是这样。现在,让我们来看看我的拖放列表框示例。
工作原理
这个列表框类演示了如何重新排列列表框项,以及利用 OLE 的拖放功能在两个列表框之间拖放项。概念非常简单:获取用户想要拖动的项,创建一个 COleDataSource
对象,向其附加数据,然后调用 COleDataSource.DoDragDrop()
。在接收端,通过重写 COleDropTarget
的 OnDragOver
方法来指示当用户拖动项时放置的位置,最后在用户释放鼠标按钮(调用 COleDropTarget
的 OnDrop
方法)时将项插入其新位置。
为了完成这项任务,我创建了一个新类 COleDragAndDropListBox
,它继承自 CListBox
和 COleDropTarget
。我更喜欢这种方法而不是组合方法,因为这样,派生自 COleDropTarget
的类不必向派生自 CListBox
的类发送消息来获取信息和通知放置操作。(多重继承部分不允许在 VC++ 6.0 及更早版本中使用,因此这仅在编译使用 VC++ 7.0 编译器时才有效。)
让我们从任务的拖动部分开始。我们需要捕获您的列表框窗口的三个消息来实现此目的,分别是 WM_LBUTTONDOWN
、WM_MOUSEMOVE
和 WM_LBUTTONUP
。
WM_LBUTTONDOWN
处理程序方法简单地确定用户选择了哪个项。
void COLEDragAndDropListBox::OnLButtonDown(UINT nFlags, CPoint point) { __super::OnLButtonDown(nFlags, point); //keep track of the item that was clicked on //WM_MOUSEMOVE message is going to use that to //create the COleDataSource m_Interval = 0; m_DropIndex = LB_ERR; m_DraggedIndex = LB_ERR; BOOL Outside; int Index = ItemFromPoint(point,Outside); if (Index != LB_ERR && !Outside) { m_DraggedIndex = Index; SetCurSel(Index); } }
WM_WMMOUSEMOVE
处理程序的唯一任务是创建一个 COleDataSource
,向其附加数据,并调用 DoDragDrop
。由于并非所有人都会简单地将文本从一个列表框传输到另一个列表框,因此 CDragAndDropListBox
类定义了一些虚拟函数来获取可用的放置模式、获取数据以及放置项。当发生需要某项内容的情况时,将使用这些虚拟函数中的一个来获取所需信息。OnMouseMove
是第一个使用这些虚拟函数的方法。它调用 GetData()
来获取应附加到 COleDataSource
的数据,之后调用 DoDragDrop()
,如果操作是移动操作,它会调用 RemoveItem()
来删除该项。
void COLEDragAndDropListBox::OnMouseMove(UINT nFlags, CPoint point) { if (m_DraggedIndex != LB_ERR && (nFlags & MK_LBUTTON)) { //create the COleDataSource, and attach the data to it COleDataSource DataSource; HGLOBAL hData = GetData(m_DraggedIndex); if (hData) { //attach the data DataSource.CacheGlobalData(m_cfFormat,hData); //allow the user to drag it. DROPEFFECT DropEffect = DataSource.DoDragDrop(GetDragItemEffects(m_DraggedIndex)); //if the user wanted to move the item then delete it //Only do this if it was dragged to another window //OnDrop handles deleting a moved item within the same window if (DropEffect & DROPEFFECT_MOVE && m_DraggedIndex != LB_ERR) { RemoveItem(m_DraggedIndex); } m_DraggedIndex = LB_ERR; GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), LBN_SELCHANGE),(LPARAM)CListBox::m_hWnd); } } __super::OnMouseMove(nFlags, point); }
WM_LBUTTONUP
消息处理程序的任务非常简单。它只需将所有变量重置为其初始状态,以供下一次拖动操作使用。
void COLEDragAndDropListBox::OnLButtonUp(UINT nFlags, CPoint point) { //cancel everything KillTimer(TID_SCROLLDOWN); KillTimer(TID_SCROLLUP); m_Interval = 0; m_DropIndex = LB_ERR; Invalidate(); __super::OnLButtonUp(nFlags, point); }
COLEDragAndDropListBox
继承的 COleDropTarget
类帮助我们处理放置端的事宜。此类有四个我们感兴趣的虚拟函数:OnDragEnter
、OnDragOver
、OnDragLeave
和 OnDrop
。我们对 OnDragEnter
和 OnDragOver
的实现几乎相同。唯一的区别是 OnDragEnter
通过调用 ActivateWindow
虚拟方法为类提供了激活窗口的机会。
DROPEFFECT COLEDragAndDropListBox::OnDragEnter(CWnd* pWnd, COleDataObject* pDataObject,DWORD dwKeyState, CPoint point) { if (DragOriginateInSameWindow() && !GetCanInternalDrop()) { return DROPEFFECT_NONE; } //if the data is the kind we want if (pDataObject->IsDataAvailable(m_cfFormat)) { //bring window to top ActivateWindow(); //draw the line where it would be inserted DrawTheLines(GetItemAt(point)); //scroll if needed DoTheScrolling(point); //return how the user can drop the item return GetDropItemEffects(pDataObject,dwKeyState); } return DROPEFFECT_NONE; } DROPEFFECT COLEDragAndDropListBox::OnDragOver(CWnd* pWnd, COleDataObject* pDataObject,DWORD dwKeyState,CPoint point) { if (DragOriginateInSameWindow() && !GetCanInternalDrop()) { return DROPEFFECT_NONE; } //if the data is the kind we want if (pDataObject->IsDataAvailable(m_cfFormat)) { //draw the line where it would be inserted DrawTheLines(GetItemAt(point)); //scroll if needed DoTheScrolling(point); //return how the user can drop the item return GetDropItemEffects(pDataObject,dwKeyState); } return DROPEFFECT_NONE; }
OnDragLeave
方法甚至更简单。它所做的只是进行一些清理工作。由于 OnDragOver
方法在列表框上绘制了一条线来指示插入点,因此在鼠标在拖动操作期间离开窗口的情况下,OnDragLeave
将负责擦除这条线。
void COLEDragAndDropListBox::OnDragLeave(CWnd* pWnd) { //cancel everything KillTimer(TID_SCROLLDOWN); KillTimer(TID_SCROLLUP); //Clear The line that was drawn CDC *pDC = GetDC(); ClearOldLine(pDC,m_DropIndex); ReleaseDC(pDC); m_Interval = 0; m_DropIndex = LB_ERR; }
OnDrop
方法是此类中最 umfangreiche 的方法。顾名思义,当用户在窗口上释放鼠标按钮时会调用它。通常,这会非常简单,但由于用户可以在源拖放操作的同一窗口内放置,因此我们必须处理一些特殊情况。特殊情况是用户想将项从一个索引移动到另一个索引。这可能很棘手,因为当我们向列表框添加或删除项时,索引会发生变化,我们很容易丢失需要插入或删除项的索引。
BOOL COLEDragAndDropListBox::OnDrop(CWnd* pWnd, COleDataObject* pDataObject,DROPEFFECT dropEffect,CPoint point) { BOOL Ret = FALSE; if (DragOriginateInSameWindow() && !GetCanInternalDrop()) { return FALSE; } //if it is a format that we handle if (pDataObject->IsDataAvailable(m_cfFormat)) { //get the index of where the user want's to insert item int Index = GetItemAt(point); HGLOBAL hGlobal = pDataObject->GetGlobalData(m_cfFormat); //if the Drag was initiated in the same window as the drop and //user wants to move the item then we have to handle the delete here //because if the original item was at a higher index than the dropped index //we would have to delete the dragged item first before we //insert the new item if (DragOriginateInSameWindow() && dropEffect == DROPEFFECT_MOVE) { if (m_DraggedIndex < Index) { Ret = DroppedAt(Index,hGlobal); RemoveItem(m_DraggedIndex); m_DraggedIndex = LB_ERR; SetCurSel(Index-1); } else if (m_DraggedIndex > Index) { RemoveItem(m_DraggedIndex); m_DraggedIndex = LB_ERR; Ret = DroppedAt(Index,hGlobal); } } else //simply drop the item in the desired index { Ret = DroppedAt(Index,hGlobal); GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(),LBN_SELCHANGE), (LPARAM)CListBox::m_hWnd); } } m_DropIndex = LB_ERR; return Ret; }
使用代码
COLEDragAndDropListBox
类有两种使用方式。如果您只想将文本从一个列表框移动到另一个列表框,那么请将您的变量类型从 CListBox
更改为 COLEDragAndDropListBox
。
// CDragDropListBoxSampleDlg dialog class CDragDropListBoxSampleDlg : public CDialog { ...... // Implementation protected: COLEDragAndDropListBox m_ListBox; ...... };
但是,如果您想发送比纯文本更多信息,那么您将需要创建一个继承自 COLEDragAndDropListBox
的类,并重写几个虚拟方法来为其提供数据并处理项的插入和删除。
// CDragDropListBoxSampleDlg dialog class CMyDragAndDropListBox : public COLEDragAndDropListBox { ...... protected: //overridables //override to supply data to attach to COleDataSource virtual HGLOBAL GetData(int ForIndex); //override to handle the item insertion virtual BOOL DroppedAt(int InsertBefore,HGLOBAL hGlobal); //override to so that you can specify //if the DROPPED item can be moved or copied or both virtual DROPEFFECT GetDropItemEffects(COleDataObject* pDataObject,DWORD dwKeyState); //override to so that you can specify //if the DRAGGED item can be moved or copied or both virtual DROPEFFECT GetDragItemEffects(int Index); //override to handle item deleting when an item is moved virtual void RemoveItem(int Index); //you can override this method to return //the index of the item that is under the point //the default implementation depends //on the Outside variable passed to ItemFromPoint //but that does not work in all cases virtual int GetItemAt(CPoint Point); //this function gets called from the OnDragOver method. //the default implementation tries to active the parent frame first, //if there is no parent frame then it activates the parent window virtual void ActivateWindow(); ...... };