控件“樹”中多選拖放功能的實現
控件”樹”(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(),它可以作為您編寫自己的回調函數的一個參考。
附:文件DRAGTREE.ZIP可從網址www.pinpub.com/vcd/下載得到
from:http://www.microsoft.com/china/msdn/library/langtool/vcpp/0214a.mspx?mfr=true
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成