控件“树”中多选拖放功能的实现
发布日期: 2/14/2002 | 更新日期: 6/30/2004
控件”树”(tree)能够清晰地显示所包含数据的继承关系,是一个强有力的控件。但是真正掌握并能运用CTreeCtrl类的众多特性并不是一件容易的事情。在Visual C++ Developer杂志的9月份、10月份这两期中,Stephen介绍了两个类CBitmapTree和CCheckableTree的用法,这两个类可以扩展和简化类CTreeCtrl。在这篇文章里,将主要介绍类CBitmapTree的升级版,它可以简化往”树”控件中添加拖放功能的过程,而且还可以实现多选拖放功能。
在CTreeCtrl类中,拖放特性是很难掌握和应用的特性之一。由于该特性能给拖放操作带来许多特有的灵活性,所以MFC组专门指派程序员负责这方面函数的编写工作,这将使得用户在开发的应用程序中实现控件”树”的拖放功能不再"望而生畏"。这里将介绍类CBitmapTree中扩充的有关成员函数,以便简化控件”树”的拖放功能的实现,同时还将该特性扩展到多选拖放。
本文所做的介绍是假定您已经对9、10月份Visual C++ Developer杂志中有关文章里面的概念和技术有所了解。这两篇文章的题目是 A Tree Control with Multiple Selection and Simpler Image Handling〖1〗和A Tree Control with Three-State Check Boxes and Sophisticated Sorting〖2〗。另外,升级的类CCheckableTree [CHECK.ZIP]文件可以从网址:www.pinpub.com/vcd/下的Subscriber Downloads部分中下载得到。
可拖动的项和释放的目标位置(Draggable items and drop targets)
拖放操作主要包含两个行为对象:被拖动的项和释放的目标位置项。为了支持可反复进行的拖放操作功能,需要能够识别出树控件类中哪些项是可拖动的以及它们的释放位置如何,而不是去识别父窗口类中的项。通过一个成员变量m_draggable中某一特定位值的设置可以设置某项的拖动特性,就象一个树的图象可以设置成"可扩充"特性一样。函数SetDraggable()、IsDraggable()、SetDropTarget()和IsDropTarget()中就使用了该成员变量m_draggable。下面是函数SetDraggable()的代码:
void CBitmapTree::SetDraggable(int base)
{
ASSERT(base < (sizeof(m_draggable)*8 -1));
m_draggable|= (1UL << base);
}
熟悉类CBitmapTree的读者可能注意到了该函数与SetExpandable()函数极其相似。变量base指明了所选的图象在"树"控件TVISL_NORMAL图象列表中的索引号。该索引号将转换成一个单位值,并且存储在成员变量m_draggable中。m_draggable成员变量类型为长整型,占4个字节32位,所以变量base指定的是位于图象列表中前32个图象中的某一个图象。而成员函数IsDraggable()可以用来查询变量m_draggable的值。成员函数IsDraggable()和IsDropTarget()都定义为虚函数,所以在需要对可拖动特性和可释放特性进行更为复杂的判断时,用户可以在类CBitmapTree的派生类中超越(overriding)该函数。网址www.pinpub.com/vcd下Subscriber Downloads部分中所包含的程序示例中,所有的文档节点都是可拖动的,所有的文件夹节点都可以作为拖动后释放的目标位置项。
准备拖动(Getting ready to drag)
当成功地执行完一次拖放操作后,函数SetDrag()就会建立一个回调函数来处理树项的移动或复制。笔者曾经在前面的文章里提供了一个比较简单的回调函数MoveTreeItemCB(),它可以完成树项在"树"控件中从一个位置移动到另一个位置。注意:这个函数本身的局限性很强,如果想完成某些该函数不具备的其它方面的功能时,就需要用户根据实际需要编写自己的回调函数。(例如,当使用宏LPSTR_TEXTCALLBACK来替代表示树项的文本时,函数MoveTreeItemCB()就不能正常运行。)
另外,在拖动树项时,可以提供给函数SetDrag()一个位图的资源标识符(并不是必需的)。该位图可以作为一种提示图标,它应该具有如图1所示的格式。当拖动超出了有效范围的边界时,则显示第一个图象;当拖动项只有一项时,则显示第二个图象;当拖动项包含多项时,则显示第三个图象。如果在程序中并没有提供图象给函数SetDrag(),那么将显示一个缺省的拖动图象,即当前所选项的灰色显示。
图1:拖放操作过程中显示的拖放图象
开始拖动(Beginning a drag)
拖动操作开始时,"树"控件的父窗口将接收到一个通知消息TVN_BEGINDRAG。您可以向CBitmapTree类的消息映象中发送一通知消息ON_NOTIFY_REFLECT_EX,以此来截获通知消息TVN_BEGINDRAG,并将其同时发送给父窗口和"树"控件,下面就是TVN_BEGINDRAG的消息处理函数:
BOOL CBitmapTree::OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
HTREEITEM h= pNMTreeView->itemNew.hItem;
int img, simg;
CPoint point;
if ((m_dragFunction == NULL) || (h == NULL))
return(FALSE);
ASSERT(GetNormalImageCount());
if (m_multiSelected)
{
// 关闭不可拖动项的选择操作
DoForAllSelected(PrepareDrag, NULL, 0);
}
else
{
// 如果已经开始拖动一个"非选择项",
// 则需要在拖动前改变该项的拖动特性
SelectItem(h);
}
// 能拖动此项吗?
GetItemImage(h, img, simg);
if (!(IsDraggable(h, img)))
return(FALSE);
// 设置拖动图象列表
if (m_ilDrag.m_hImageList != NULL)
m_dragList= &m_ilDrag;
else
{
m_dragList= CreateDragImage(h);
if (m_dragList == NULL)
return(FALSE);
}
if (m_multiSelected)
m_dragList->BeginDrag(m_multiDrag, CPoint(0,0));
else
m_dragList->BeginDrag(m_docDrag, CPoint(0,0));
MapWindowPoints(NULL, &pNMTreeView->ptDrag, 1);
m_dragList->DragEnter(NULL, pNMTreeView->ptDrag);
ShowCursor(FALSE);
SetCapture();
// 允许边拖动边滚动"树"控件窗口
SetTimer(EVENT_DRAG_SCROLL, 250, NULL);
*pResult= 0;
return(FALSE);
}
当调用OnBeginDrag()函数时,首先判断是否为多节点拖动,当m_multiSelected值为真,即为多节点拖动时,将调用PrepareDrag()函数来关闭那些未声明为"可拖动"项的选择操作(负责调用函数PrepareDrag()的函数DoForAllSelected()在文章〖1〗中有所描述)。然后再调用函数IsDraggable()检验一下当前所选的项是否都是可拖动项,如果有不可拖动项,则函数返回FALSE,否则,继续下面的语句,初始化拖动操作过程中显示的图象列表,调用函数BeginDrag()。MSDN文档中解释说函数BeginDrag()中的第二个参量为"起始拖动位置的坐标(典型情况下为光标位置)"。文档中建议第二个参量使用光标的当前位置坐标,并且将其存储在pNMTreeView->ptDrag中,事实上,这也正是程序示例MFCTREE中所采用的方法。但是,最近的MSDN应用程序示例CMNCTRLS和TREESCR中使用的是(0,0),上面的代码段中也使用(0,0)。OnBeginDrag()函数的后一部分调用了函数DragEnter()来初始化拖动操作。在调用完函数DragEnter()以后,如果想刷新控件"树",那么就需要首先调用DragLeave()函数。
拖动控件"树"中的项(Dragging a tree item)
在执行拖动操作的过程中,拖动图象根据操作的不同,显示上也有些变化。下面的处理程序OnMouseMove()就能够完成这些显示上的变化:
void CBitmapTree::OnMouseMove(UINT nFlags,
CPoint point)
{
if (m_dragList)
{
BOOL target_set = FALSE;
BOOL has_drag_images
= (m_ilDrag.m_hImageList != NULL);
HTREEITEM target;
CPoint screen= point;
MapWindowPoints(NULL, &screen, 1);
// 鼠标所在的位置是哪一项?
if ((target= GetItemUnderMouse()) != NULL)
{
int img, simg;
if (has_drag_images)
{
if (m_multiSelected)
m_dragList->SetDragCursorImage(m_multiDrag, CPoint(0,0));
else
m_dragList->SetDragCursorImage(m_docDrag, CPoint(0,0));
}
while (target)
{
GetItemImage(target, img, simg);
if (IsDropTarget(target, img))
{
m_dragList->DragLeave(NULL);
SelectDropTarget(target);
m_dragList->DragEnter(NULL, screen);
target_set= TRUE;
break;
}
target= GetParentItem(target);
}
}
else
{
// 超出有效区域
if (has_drag_images)
m_dragList->SetDragCursorImage(m_xDrag, CPoint(0,0));
}
if (!(target_set))
{
m_dragList->DragLeave(NULL);
SelectDropTarget(NULL);
m_dragList->DragEnter(NULL, screen);
}
}
CTreeCtrl::OnMouseMove(nFlags, point);
}
首先,如果在拖放操作的有效区域之外(如控件"树"窗口外或控件"树"窗口内的空白区)释放了鼠标左键,那么显示的拖动图象将表明"无任何操作"。当鼠标移动到拖放操作的有效区域内,拖动图象要恢复为正常状态。这些拖动图象的改变是通过把相应的图象传递给函数CImageList::SetDragCursorImage()来完成的,下面的图2表明了拖动图象的这种变化。如果调用函数SetDrag()时没有图象传递给该函数,则has_drag_images值为假,拖动图象就不会有任何改变。
在执行拖动操作的过程中,相应的释放目标项为突出显示。如果当前鼠标位置所在的项为释放的目标位置项,则通过调用CTreeCtrl::SelectDropTarget()函数就可以使该项突出显示,否则,最近的父文件夹突出显示,如果父文件夹并不是释放的目标文件夹,那么任何项都不突出显示。
图2:拖动文件夹Cats中的"The Shedding Cycle"项到文件夹Snakes中:被拖动项含有一个点线画成的边框,释放的目标文件夹则反相显示。
边拖边滚
当拖动操作位于"树"控件的有效边界时,如果能够实现"树"控件窗口的自动上滚或下滚以显示其余的树项,那将是件很漂亮的工作。使用OnTimer()消息处理函数就可以完成这项工作。函数OnBeginDrag()中调用了SetTimer()函数,该函数中设置了定时器事件(timer event)EVENT_DRAG_SCROLL,该定时器事件只有在调用OnLButtonUp消息处理函数时才被释放。
OnTimer()消息处理函数可以实现当鼠标移到控件"树"窗口边界时,控件"树"窗口的上滚或下滚操作。在MSDN的程序示例TREESCR中,应用了此函数,它可以调整窗口相对与鼠标位置的滚动速度。
拖动结束(When the drag is over)
当释放鼠标左键时,拖动操作结束。下面的OnLButtonUp()消息处理函数可以完成拖动结束时的各种事后处理工作:
void CBitmapTree::OnLButtonUp(UINT nFlags,
CPoint point)
{
if (m_dragList)
{
HTREEITEM target;
KillTimer(EVENT_DRAG_SCROLL);
ReleaseCapture();
m_dragList->EndDrag();
ShowCursor(TRUE);
m_dragList= NULL;
if ((target= GetDropHilightItem()) != NULL)
{
DoForAllSelected(m_dragFunction, NULL,
(LPARAM) target);
SelectDropTarget(NULL);
SelectItem(target);
}
ClearMultiSelect();
}
CTreeCtrl::OnLButtonUp(nFlags, point);
}
首先进行一些简单的拖放事件或函数的清理工作,然后判断是否有突出显示的释放目标项。如果有突出显示的释放目标项,则要利用回调函数。正如前面提到的那样,您可以编写自己的回调函数来处理您所希望的加入到控件"树"中的各种复杂功能。笔者为您提供的只是一个比较简单的回调函数MoveTreeItemCB(),它可以作为您编写自己的回调函数的一个参考。