关于duilib的CTreeViewUI扩展以支持节点拖放的手记

14 篇文章 1 订阅
3 篇文章 0 订阅

本文主要是记录下对于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的,发现节点的默认展开与折叠状态有问题,时间关系,我没处理,如果哪位优化了这里,请一定发个邮件我:182534287@qq.com

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

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值