樹型控件用來顯示具有一定層次結構的數據項時方便、直觀,被廣泛地應用在各種軟件中,如資源管理器中的磁盤目錄就用的是樹型控件,我們在編程中也會經常用到,但 MFC 中提供的 CTreeCtrl 類並不直接支持拖動節點等高級特性,這使我們程序員編程時有很大限制,又給軟件用戶帶來了一些不便。下面就讓我們自己動手來解決這個問題,實現樹型控件中節點的拖動。
我們從 CTreeCtrl 中派生了一個類 CXTreeCtrl ,它具有如下的特點:
⑴ 基本拖動的實現。
⑵ 處理無意拖動。
⑶ 能處理拖動過程中的滾動問題。
⑷ 拖動過程中節點會智能展開。
圖 1 為示例程序的運行界面。
(圖 1)
好,我們來一步一步實現上述功能。
新建一對話框工程,編輯資源,在對話框中加入一樹型控件 IDC_TREE ,屬性設置如圖 2,給該控件添加一個成員變量 m_wndTree ,
類型改為CXTreeCtrl。從 CTreeCtrl 中派生一個類 CXTreeCtrl 。
(圖 2)
1、基本拖動的實現
當我們要拖動一個項目時,樹型視圖控件會給它的父窗口發送TVN_BEGINDRAG通知消息。可以在此處創建表示項目處在拖動操作中
的圖象,調用 CreateDragImage 函數產生一副缺省的圖象,該函數創建的圖象由條目圖象和標簽文本組成。創建了拖動圖象後,調用
BeginDrag 函數指定拖動圖象的熱點位置,然後調用 DragEnter 函數顯示拖動圖象。接下來處理 WM_MOUSEMOVE 消息用於更新拖動圖
象,我們想讓移動中的圖象經過某些項目時高亮度顯示,這可以調用 SelectDropTarget 來實現。在調用 SelectDropTarget 前,我們先調用
DragShowNolock ( false ) 來隱藏圖象列表,之後再調用 DragShowNolock ( true ) 來恢復圖象列表的顯示,這樣就不會在拖動過程中留下難
看的軌跡。最後我們處理 WM_LBUTTONUP 消息用於完成拖動操作,在給消息中,我們需要完成結束拖動圖想的顯示、刪除拖動圖象、釋
放鼠標、節點的拷貝/刪除等操作。在節點的拷貝/刪除操作中,如果是父節點拖到子節點上,我們可以先將父節點拷到根結點下的臨時節點
中,再從臨時結點處拷到子節點,然後將根結點下的臨時節點刪除,這樣做的目的是防止產生異常。
2、處理無意拖動
大家可能都有過這樣的經歷:在鼠標按下時不小心移動了鼠標,這時系統就認為產生了一個移動操作。如果我們不針對這種情況加以解
決的話,就很容易產生誤操作。下面我們就提出一個解決方法:設置時間延遲。也就是說當用戶按下鼠標後必須在原位置停留一段時間,才
能激活拖動操作。
3、處理拖動過程中的滾動問題
當我們進行拖動時,如果目的節點不可見,則需要拖動滾動條或收攏其它一些節點以使得目的節點顯示出來,無疑,這會給我們帶來很
大的不便。下面我們就來給樹型控件添加自動滾動支持。
設置一個定時器,在 WM_TIMER 消息中檢測鼠標的位置,如果靠近樹型控件的下邊緣,則使得控件向下滾動。靠近上邊緣則向上滾動。
滾動速度根據鼠標的位置確定。
4、拖動過程中節點的智能展開
這一步我們要實現的功能是在拖動過程中當鼠標停留在某個節點上一段時間後,該節點會自動展開。
設置一個定時器,當鼠標在拖動過程中停止在某個節點上時,定時器被啟動,再設置一變量保存當前的鼠標位置。
下面是實現的源代碼
// XTreeCtrl.h
……
protected:
UINT m_TimerTicks; //處理滾動的定時器所經過的時間
UINT m_nScrollTimerID; //處理滾動的定時器
CPoint m_HoverPoint; //鼠標位置
UINT m_nHoverTimerID; //鼠標敏感定時器
DWORD m_dwDragStart; //按下鼠標左鍵那一刻的時間
BOOL m_bDragging; //標識是否正在拖動過程中
CImageList* m_pDragImage; //拖動時顯示的圖象列表
HTREEITEM m_hItemDragS; //被拖動的標簽
HTREEITEM m_hItemDragD; //接受拖動的標簽
……
// XTreeCtrl.cpp
……
#define DRAG_DELAY 60
……
void CXTreeCtrl::OnBegindrag(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
*pResult = 0;
//如果是無意拖動,則放棄操作
if( (GetTickCount() - m_dwDragStart) < DRAG_DELAY )
return;
m_hItemDragS = pNMTreeView->itemNew.hItem;
m_hItemDragD = NULL;
//得到用於拖動時顯示的圖象列表
m_pDragImage = CreateDragImage( m_hItemDragS );
if( !m_pDragImage )
return;
m_bDragging = true;
m_pDragImage->BeginDrag ( 0,CPoint(8,8) );
CPoint pt = pNMTreeView->ptDrag;
ClientToScreen( &pt );
m_pDragImage->DragEnter ( this,pt ); //"this"將拖動操作限制在該窗口
SetCapture();
m_nScrollTimerID = SetTimer( 2,40,NULL );
}
void CXTreeCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
HTREEITEM hItem;
UINT flags;
//檢測鼠標敏感定時器是否存在,如果存在則刪除,刪除後再定時
if( m_nHoverTimerID )
{
KillTimer( m_nHoverTimerID );
m_nHoverTimerID = 0;
}
m_nHoverTimerID = SetTimer( 1,800,NULL ); //定時為 0.8 秒則自動展開
m_HoverPoint = point;
if( m_bDragging )
{
CPoint pt = point;
CImageList::DragMove( pt );
//鼠標經過時高亮顯示
CImageList::DragShowNolock( false ); //避免鼠標經過時留下難看的痕跡
if( (hItem = HitTest(point,&flags)) != NULL )
{
SelectDropTarget( hItem );
m_hItemDragD = hItem;
}
CImageList::DragShowNolock( true );
//當條目被拖曳到左邊緣時,將條目放在根下
CRect rect;
GetClientRect( &rect );
if( point.x < rect.left + 20 )
m_hItemDragD = NULL;
}
CTreeCtrl::OnMouseMove(nFlags, point);
}
void CXTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
CTreeCtrl::OnLButtonUp(nFlags, point);
if( m_bDragging )
{
m_bDragging = FALSE;
CImageList::DragLeave( this );
CImageList::EndDrag();
ReleaseCapture();
delete m_pDragImage;
SelectDropTarget( NULL );
if( m_hItemDragS == m_hItemDragD )
{
KillTimer( m_nScrollTimerID );
return;
}
Expand( m_hItemDragD,TVE_EXPAND );
HTREEITEM htiParent = m_hItemDragD;
//如果是由父節點拖向子節點
while( (htiParent = GetParentItem(htiParent)) != NULL )
{
if( htiParent == m_hItemDragS )
{
//建立一個臨時節點以完成操作
HTREEITEM htiNewTemp = CopyBranch( m_hItemDragS,NULL,TVI_LAST );
HTREEITEM htiNew = CopyBranch( htiNewTemp,m_hItemDragD,TVI_LAST );
DeleteItem( htiNewTemp );
SelectItem( htiNew );
KillTimer( m_nScrollTimerID );
return;
}
}
HTREEITEM htiNew = CopyBranch( m_hItemDragS,m_hItemDragD,TVI_LAST );
DeleteItem( m_hItemDragS );
SelectItem( htiNew );
KillTimer( m_nScrollTimerID );
}
}
//拷貝條目
HTREEITEM CXTreeCtrl::CopyItem(HTREEITEM hItem, HTREEITEM htiNewParent, HTREEITEM htiAfter)
{
TV_INSERTSTRUCT tvstruct;
HTREEITEM hNewItem;
CString sText;
//得到源條目的信息
tvstruct.item.hItem = hItem;
tvstruct.item.mask = TVIF_CHILDREN|TVIF_HANDLE|TVIF_IMAGE|TVIF_SELECTEDIMAGE;
GetItem( &tvstruct.item );
sText = GetItemText( hItem );
tvstruct.item.cchTextMax = sText.GetLength ();
tvstruct.item.pszText = sText.LockBuffer ();
//將條目插入到合適的位置
tvstruct.hParent = htiNewParent;
tvstruct.hInsertAfter = htiAfter;
tvstruct.item.mask = TVIF_IMAGE|TVIF_SELECTEDIMAGE|TVIF_TEXT;
hNewItem = InsertItem( &tvstruct );
sText.ReleaseBuffer ();
//限制拷貝條目數據和條目狀態
SetItemData( hNewItem,GetItemData(hItem) );
SetItemState( hNewItem,GetItemState(hItem,TVIS_STATEIMAGEMASK),TVIS_STATEIMAGEMASK);
return hNewItem;
}
//拷貝分支
HTREEITEM CXTreeCtrl::CopyBranch(HTREEITEM htiBranch, HTREEITEM htiNewParent, HTREEITEM htiAfter)
{
HTREEITEM hChild;
HTREEITEM hNewItem = CopyItem( htiBranch,htiNewParent,htiAfter );
hChild = GetChildItem( htiBranch );
while( hChild != NULL )
{
CopyBranch( hChild,hNewItem,htiAfter );
hChild = GetNextSiblingItem( hChild );
}
return hNewItem;
}
void CXTreeCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
//處理無意拖曳
m_dwDragStart = GetTickCount();
CTreeCtrl::OnLButtonDown(nFlags, point);
}
void CXTreeCtrl::OnTimer(UINT nIDEvent)
{
//鼠標敏感節點
if( nIDEvent == m_nHoverTimerID )
{
KillTimer( m_nHoverTimerID );
m_nHoverTimerID = 0;
HTREEITEM trItem = 0;
UINT uFlag = 0;
trItem = HitTest( m_HoverPoint,&uFlag );
if( trItem && m_bDragging )
{
SelectItem( trItem );
Expand( trItem,TVE_EXPAND );
}
}
//處理拖曳過程中的滾動問題
else if( nIDEvent == m_nScrollTimerID )
{
m_TimerTicks++;
CPoint pt;
GetCursorPos( &pt );
CRect rect;
GetClientRect( &rect );
ClientToScreen( &rect );
HTREEITEM hItem = GetFirstVisibleItem();
if( pt.y < rect.top +10 )
{
//向上滾動
int slowscroll = 6 - (rect.top + 10 - pt.y )/20;
if( 0 == (m_TimerTicks % ((slowscroll > 0) ? slowscroll : 1)) )
{
CImageList::DragShowNolock ( false );
SendMessage( WM_VSCROLL,SB_LINEUP );
SelectDropTarget( hItem );
m_hItemDragD = hItem;
CImageList::DragShowNolock ( true );
}
}
else if( pt.y > rect.bottom - 10 )
{
//向下滾動
int slowscroll = 6 - (pt.y - rect.bottom + 10)/20;
if( 0 == (m_TimerTicks % ((slowscroll > 0) ? slowscroll : 1)) )
{
CImageList::DragShowNolock ( false );
SendMessage( WM_VSCROLL,SB_LINEDOWN );
int nCount = GetVisibleCount();
for( int i=0 ; i<nCount-1 ; i++ )
hItem = GetNextVisibleItem( hItem );
if( hItem )
SelectDropTarget( hItem );
m_hItemDragD = hItem;
CImageList::DragShowNolock ( true );
}
}
}
else
CTreeCtrl::OnTimer(nIDEvent);
}
通過上面的代碼我們就實現了樹型控件的拖動操作。
示例程序在 VC6+Windows2000 上調試通過。