關於duilib的CTreeViewUI擴展以支持節點拖放的手記

本文主要是記錄下對於CtreeViewUI支持不同節點間的拖放功能的擴展過程,拋磚引玉,希望能讓更多的人來豐富duilib的功能。

 

由於客戶要求能夠在樹控件中在各個節點間進行節點拖放,此項目是應用duilib來實現的,但找遍了duilib的例子以及網上的資料,都沒有相關可以拖放的樹的信息,這下可難倒我這個剛入門的duiliber了,想來想去,擬定了如下三個探索方向:

1. 嵌入windowTreeView

網上有MFC版本的現成的可拖放節點的代碼,而duilib也能支持嵌入系統控件的功能,應該是能夠通過這種方式實現的,但深入考慮下去,這條路我還是放棄了,原因有二:

1). 找到的代碼是MFC版本的,要轉成WIN32 API的版本,還需要較多時間;

2). 即便轉換完成,這裏還需要通過自繪方式來完成樹節點的美化,這恰好是我的短板,也與使用duilib不符。

 

2. 在程序中嵌入一個網頁,通過JS的方式來實現節點拖放

找到網上有相關的JS可拖動節點的樹的實現,如果能做到與程序的完美交互[每一次拖放節點,均需要將移動過的節點信息保存進本地數據庫],這應該是一個不錯的辦法。

 

3. 使用duilib來進行擴展

從內心來講,還是希望使用duilib來原生的支持拖放功能,這樣無論是加入業務處理還是顯示效果,都會是最完美的;同時心裏又實在沒底,擔心擴展失敗。直到寫此文的時候,也不知道目前的狀態是不是就已經能夠滿足要求了,所以第2個方案還是作爲備選,萬一擴展的方案最終行不通,也至少有可以完成項目的方案。

 

 

我所設想的拖放功能,應該是移動 + 拖放效果,而這裏的核心應該是在移動上,應該如何來實現移動呢?

1. 節點的移動

CTreeViewUI是繼承於CListUI的,無論樹中的每一個節點處於何層次,均是以CListUI中的行數據來呈現的,而每一行則是以CTreeNodeUI來呈現。

 

通過對CTreeViewUICTreeNodeUI的代碼分析,以及結合duilib的控件指針智能管理的處理,我決定按此思路進一步處理:

在要移動一個節點時,分解成 移除 + 添加,移除舊的節點,在新的位置添加此節點[以及其子節點],那這裏就需要做到之前移除節點時,僅是將其從CListUI中移除,而不是會銷燬這個節點控件,這樣才能保證後續能將其添加到新的位置上。

 

這裏我們注意到CTreeNodeUIRemoveAt函數,此函數主要是刪除自身的所有子節點,再將自己從樹pTreeView中刪除,進而調用CListUI::Remove(),最終落在CContainerUI的刪除控件的函數中:

bool CContainerUI::Remove(CControlUI* pControl)
{
if( pControl == NULL) return false;
 
for( int it = 0; it < m_items.GetSize(); it++ ) {
if( static_cast<CControlUI*>(m_items[it]) == pControl ) {
NeedUpdate();
this;
if( m_bAutoDestroy ) {
if( m_bDelayedDestroy && m_pManager ) m_pManager->AddDelayedCleanup(pControl);             
else delete pControl;
}
return m_items.Remove(it);
}
}
return false;
}

CContainnerUI有一個m_bAutoDestroy屬性,決定它在刪除控件的時候是否自動清理內存,所以我們只要能設置好容器不要幫我們清理內容,這個控件就能得以保存下來,以供下次使用了。

經過測試跟蹤,通過CtreeViewUI SetAutoDestroy(false);設置容器不自動清理,總是在CContainnerUIRemove函數中發現m_bAutoDestroy 爲true[默認值],最終發現是由於CListBodyUI的的屬性未正確設置,需要在CTreeViewUI中添加如下代碼:

void CTreeViewUI::SetAutoDestroy(bool bAuto)
{
m_pList->SetAutoDestroy(bAuto);
__super::SetAutoDestroy(bAuto);
}

解決了控件的生命週期的問題,接下來需要將其添加到具體的位置了,這裏需要注意的,如果被Move的節點有子節點,直接將此節點添加到目標節點下是有問題的,他的子節點將不會顯示,此處我的做法是針對本節點,以及其所有子節點,逐個添加到新的位置。

爲此,我在CTreeViewUI中添加了一個Move的函數來總管移動節點,以及在CTreeNodeUI中添加了AddToList以及AddNodeFromList兩個函數,這兩個函數主要是完成對被移動節點的子節點的添加[刪除時已經被從CList中移除]

 

void CTreeViewUI::Move(CTreeNodeUI* dstParent, CTreeNodeUI* pNode)
{
if (dstParent == NULL || pNode == NULL)
{
return;
}
CStdPtrArray listNodes;
CTreeNodeUI* srcParent = pNode->GetParentNode();
if (srcParent == NULL)
{
return;
}
pNode->AddToList(listNodes);
 
SetAutoDestroy(false);	//移除前先設置不自動清除
srcParent->Remove(pNode);
SetAutoDestroy(true);	//還原默認設置
 
dstParent->AddChildNode(pNode);
pNode->SetParentNode(dstParent);
pNode->AddNodeFromList(listNodes);
}
 
//輔助遍歷添加子節點的結構
class MoveNode
{
public:
CTreeNodeUI* pNode;
CStdPtrArray childList;
};
 
void CTreeNodeUI::AddNodeFromList(CStdPtrArray &dstList)
{
for (int i=0; i<dstList.GetSize(); i++)
{
MoveNode* moveNode = static_cast<MoveNode*>(dstList.GetAt(i));
AddChildNode(moveNode->pNode);
moveNode->pNode->pParentTreeNode = this;
moveNode->pNode->AddNodeFromList(moveNode->childList);
delete moveNode;
}
 
}
 
void CTreeNodeUI::AddToList(CStdPtrArray &dstList)
{
if (IsHasChild())
{
int nChildCount = GetCountChild();
for (int i=0; i<nChildCount; i++)
{
CTreeNodeUI* pNode = GetChildNode(i);
MoveNode* pMoveNode = new MoveNode;
pMoveNode->pNode = pNode;
dstList.Add(pMoveNode);
pNode->AddToList(pMoveNode->childList);
}
}
}
 

通過以上修改,實現了節點從一個位置到另外一個位置的移動,測試代碼如下:

CTreeViewUI* pTree = static_cast<CTreeViewUI*>(m_pm.FindControl(_T("tree")));
CTreeNodeUI* dstParent = (CTreeNodeUI*)pTree->GetItemAt(1);
CTreeNodeUI* pNode = (CTreeNodeUI*)pTree->GetItemAt(6);
pTree->Move(dstParent, pNode);

2. 拖放實現

在已經實現了控件移動的基礎上,要實現拖放就變得簡單了,無非就是通過鼠標按下確定需要被移動的節點,通過鼠標彈起事件來確定應該被移動到哪個節點下。

可以通過重載CTreeNodeUIDoEvent函數來實現,分別處理UIEVENT_BUTTONDOWNUIEVENT_BUTTONUPUIEVENT_MOUSEMOVE,如下:

void CTreeNodeUI::DoEvent( TEventUI& event )
{
if( event.Type == UIEVENT_DBLCLICK )
{
if( IsEnabled() ) {
m_pManager->SendNotify(this, DUI_MSGTYPE_ITEMDBCLICK);
Invalidate();
}
return;
}
else if (event.Type == UIEVENT_BUTTONDOWN)
{
CTreeNodeUI* pNode = GetFirstCTreeNodeUIFromPoint(event.ptMouse);
pTreeView->BeginDrag(pNode);
}
else if (event.Type == UIEVENT_BUTTONUP)
{
CTreeNodeUI* pNode = GetFirstCTreeNodeUIFromPoint(event.ptMouse);
pTreeView->EndDrag(pNode);
}
else if (UIEVENT_MOUSEMOVE == event.Type)
{
pTreeView->Draging(event.ptMouse);
}
CListContainerElementUI::DoEvent(event);
}

這裏需要注意的是,當鼠標按下時,觸發此事件的控件並不是CTreeNodeUI,而是其子控件,所以這裏需要通過獲取其父窗口來得到CTreeNodeUI;另外,在鼠標彈起時,更加需要注意,此時通過鼠標位置獲取到的控件並不是CTreeNodeUI,也是需要上溯N層才能得到,這裏通過GetFirstCTreeNodeUIFromPoint來實現:

CTreeNodeUI* CTreeNodeUI::GetFirstCTreeNodeUIFromPoint(POINT pt)
{
LPVOID lpControl = NULL;
CControlUI* pControl = m_pManager->FindSubControlByPoint(pTreeView, pt);
while(pControl)
{
lpControl = pControl->GetInterface(DUI_CTR_TREENODE);
if (lpControl != NULL)
{
break;
}
pControl = pControl->GetParent();
}
if(lpControl)
{
return static_cast<CTreeNodeUI*>(lpControl);
}
else
return NULL;
}

 

CTreeViewUI中,添加三個函數來處理鎖定被拖放節點、拖放效果、完成控件移動三個操作,此處需要注意的是,要對將某節點往自己的子節點移動的情況,這是不允許發生的:

void CTreeViewUI::BeginDrag(CTreeNodeUI* pNode)
{
m_pNodeNeedMove = pNode;
static_cast<CContainerUI*>(m_pDragingCtrl)->GetItemAt(0)->SetText(m_pNodeNeedMove->GetItemText());
} 
void CTreeViewUI::Draging(POINT pt)
{
if (m_pNodeNeedMove == NULL || m_pDragingCtrl == NULL)
{
return;
}
RECT rt;
rt.left = pt.x + 5;
rt.top = pt.y + 5;
rt.right = rt.left + 130;
rt.bottom = rt.top + 20;
m_pDragingCtrl->SetPos(rt);
}
 
void CTreeViewUI::EndDrag(CTreeNodeUI* dstParent)
{
RECT rt;
rt.left = rt.right = rt.top = rt.bottom = 0;
m_pDragingCtrl->SetPos(rt);
if (m_pNodeNeedMove != NULL && dstParent != NULL && m_pNodeNeedMove != dstParent)
{
if (m_pNodeNeedMove->GetParentNode() == dstParent)
{
m_pNodeNeedMove = NULL;
return;
}
//判斷,如果dstParent 是m_pNodeNeedMove的父節點,直接退出,避免自己往自己子節點添加的情況發生
if (IsChildNodeOfSrcNode(m_pNodeNeedMove, dstParent))
{
m_pNodeNeedMove = NULL;
return;
}
Move(dstParent, m_pNodeNeedMove);
 
//設置所有的節點均爲非選擇,然後設置之前移動的爲選擇狀態
int nCount = GetCount();
for (int i = 0; i< nCount; i++)
{
((CListContainerElementUI*)GetItemAt(i))->Select(false);
}
m_pNodeNeedMove->Select();
m_pNodeNeedMove = NULL;
}
m_pNodeNeedMove = NULL;
}
 
bool CTreeViewUI::IsChildNodeOfSrcNode(CTreeNodeUI* srcNode, CTreeNodeUI* pNode)
{
CTreeNodeUI *pTemp = pNode;
while(pTemp)
{
if(pTemp == srcNode)
{
return true;
}
pTemp = pTemp->GetParentNode();
}
return false;
}

至此,我擴展的控件功能就結束了,此處的拖動效果,一直沒有好的辦法實現,最後我採用了一個半透明的文本控件來跟隨拖動時的鼠標來實現。

看下效果圖吧:

 

Demo代碼下載地址:http://download.csdn.net/detail/tragicguy/7115053

 

還有如下功能未實現或未修復問題:

 1. 此demo改自tojen的,發現節點的默認展開與摺疊狀態有問題,時間關係,我沒處理,如果哪位優化了這裏,請一定發個郵件我:[email protected]

 2. 可能的其他不完善的地方

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章