热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

本文主要是记录下对于CtreeViewUI支持不同节点间的拖放功能的扩展过程,抛砖引玉,希望能让更多的人来丰富duilib的功能。 由于客户要求能够在树控件中在各个节点间进

本文主要是记录下对于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 if( static_cast(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{
MoveNode* moveNode = static_cast(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{
CTreeNodeUI* pNode = GetChildNode(i);
MoveNode* pMoveNode = new MoveNode;
pMoveNode->pNode = pNode;
dstList.Add(pMoveNode);
pNode->AddToList(pMoveNode->childList);
}
}

通过以上修改,实现了节点从一个位置到另外一个位置的移动,测试代码如下:

CTreeViewUI* pTree = static_cast(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(lpControl);
}
else
return NULL;
}

 

CTreeViewUI中,添加三个函数来处理锁定被拖放节点、拖放效果、完成控件移动三个操作,此处需要注意的是,要对将某节点往自己的子节点移动的情况,这是不允许发生的:

void CTreeViewUI::BeginDrag(CTreeNodeUI* pNode)
{
m_pNodeNeedMove = pNode;
static_cast(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{
((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;
}

至此,我扩展的控件功能就结束了,此处的拖动效果,一直没有好的办法实现,最后我采用了一个半透明的文本控件来跟随拖动时的鼠标来实现。

看下效果图吧:

 bubuko.com,布布扣

Demo代码下载地址:http://download.csdn.net/detail/tragicguy/7115053

 

还有如下功能未实现或未修复问题:

 1. 此demo改自tojen的,发现节点的默认展开与折叠状态有问题,时间关系,我没处理,如果哪位优化了这里,请一定发个邮件我:182534287@qq.com

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

关于duilib的CTreeViewUI扩展以支持节点拖放的手记,布布扣,bubuko.com


推荐阅读
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
author-avatar
琉璃梦0_471
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有