duilib中控件拖拽功能的实现方法(附源码)

简介: 转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/41144283         duilib库中原本没有显示的对控件增加拖拽的功能,而实际使用过程中拖拽功能也是有用武之地的。

转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/41144283


        duilib库中原本没有显示的对控件增加拖拽的功能,而实际使用过程中拖拽功能也是有用武之地的。看群里有人问题duilib怎么支持拖拽,我也就写这篇文章说明一下duilib实现控件拖拽的方法。


       当我刚接触duilib不就的时候,考虑过duilib拖拽这个功能,当时的想法是,在xml布局中设置一个浮动的控件,正常状态下他是隐藏的,当出发了拖拽条件后将他显示并且跟随鼠标移动。这个方法显然并不优雅,编写代码后,由于还要借助其他的控件,所以耦合度太高,还要受到很多条件限制,并不容易把拖拽功能封装到一个单独控件里。


       后来读完duilib的源码并且慢慢熟悉他后发现,实际上,duilib虽然没有显示的支持控件拖拽,但他自身已经必备了控件拖拽的条件和机制,并且在一些控件中已经运用!例如横纵向布局的sepwidth属性和sepimm属性来支持布局手动拖动边框改变大小,ListHeader支持拖动改变自身的宽度。


分析机制:

      这里我先分析一下横向布局CHorizontalLayoutUI的拖拽机制,两个关键属性的介绍如下:

		<Attribute name="sepwidth" default="0" type="INT" comment="分隔符宽,正负表示分隔符在左边还是右边,如(-4)"/>
		<Attribute name="sepimm" default="false" type="BOOL" comment="拖动分隔符是否立即改变大小,如(false)"/>

      sepwidth属性很简单,我就不说了。sepimm属性表示拖动布局的分隔符时是否立即改变大小,下面两幅图分别是不立即改变大小的和立即改变大小的:

       

       


        可以看到不立即改变大小的拖拽,会有一个半透明黑色阴影来表示当前拖动的位置,立即拖动就是立马改变了容器的大小。拖拽功能的实现代码主要在DoEvent函数和DoPostPaint函数中完成的,DoEvent函数代码如下:

	void CHorizontalLayoutUI::DoEvent(TEventUI& event)
	{
		if( m_iSepWidth != 0 ) {
			if( event.Type == UIEVENT_BUTTONDOWN && IsEnabled() )
			{
				RECT rcSeparator = GetThumbRect(false);
				if( ::PtInRect(&rcSeparator, event.ptMouse) ) {
					m_uButtonState |= UISTATE_CAPTURED;
					ptLastMouse = event.ptMouse;
					m_rcNewPos = m_rcItem;
					if( !m_bImmMode && m_pManager ) m_pManager->AddPostPaint(this);
					return;
				}
			}
			if( event.Type == UIEVENT_BUTTONUP )
			{
				if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
					m_uButtonState &= ~UISTATE_CAPTURED;
					m_rcItem = m_rcNewPos;
					if( !m_bImmMode && m_pManager ) m_pManager->RemovePostPaint(this);
					NeedParentUpdate();
					return;
				}
			}
			if( event.Type == UIEVENT_MOUSEMOVE )
			{
				if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
					LONG cx = event.ptMouse.x - ptLastMouse.x;
					ptLastMouse = event.ptMouse;
					RECT rc = m_rcNewPos;
					if( m_iSepWidth >= 0 ) {
						if( cx > 0 && event.ptMouse.x < m_rcNewPos.right - m_iSepWidth ) return;
						if( cx < 0 && event.ptMouse.x > m_rcNewPos.right ) return;
						rc.right += cx;
						if( rc.right - rc.left <= GetMinWidth() ) {
							if( m_rcNewPos.right - m_rcNewPos.left <= GetMinWidth() ) return;
							rc.right = rc.left + GetMinWidth();
						}
						if( rc.right - rc.left >= GetMaxWidth() ) {
							if( m_rcNewPos.right - m_rcNewPos.left >= GetMaxWidth() ) return;
							rc.right = rc.left + GetMaxWidth();
						}
					}
					else {
						if( cx > 0 && event.ptMouse.x < m_rcNewPos.left ) return;
						if( cx < 0 && event.ptMouse.x > m_rcNewPos.left - m_iSepWidth ) return;
						rc.left += cx;
						if( rc.right - rc.left <= GetMinWidth() ) {
							if( m_rcNewPos.right - m_rcNewPos.left <= GetMinWidth() ) return;
							rc.left = rc.right - GetMinWidth();
						}
						if( rc.right - rc.left >= GetMaxWidth() ) {
							if( m_rcNewPos.right - m_rcNewPos.left >= GetMaxWidth() ) return;
							rc.left = rc.right - GetMaxWidth();
						}
					}

					CDuiRect rcInvalidate = GetThumbRect(true);
					m_rcNewPos = rc;
					m_cxyFixed.cx = m_rcNewPos.right - m_rcNewPos.left;

					if( m_bImmMode ) {
						m_rcItem = m_rcNewPos;
						NeedParentUpdate();
					}
					else {
						rcInvalidate.Join(GetThumbRect(true));
						rcInvalidate.Join(GetThumbRect(false));
						if( m_pManager ) m_pManager->Invalidate(rcInvalidate);
					}
					return;
				}
			}
			if( event.Type == UIEVENT_SETCURSOR )
			{
				RECT rcSeparator = GetThumbRect(false);
				if( IsEnabled() && ::PtInRect(&rcSeparator, event.ptMouse) ) {
					::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZEWE)));
					return;
				}
			}
		}
		CContainerUI::DoEvent(event);
	}


          在DoEvent函数里主要用UIEVENT_BUTTONDOWN、UIEVENT_BUTTONUP、UIEVENT_MOUSEMOVE三个事件完成了这个功能。

         1、在UIEVENT_BUTTONDOWN事件里将m_uButtonState变量赋值为UISTATE_CAPTURED,这个标志用来表示这个容器当前是否处在拖拽状态下;ptLastMouse变量赋值为当前鼠标的坐标;m_rcNewPos赋值为当前容器所在的位置,从名字上可以看出来他表示控件的新的位置的区域。然后根据m_bImmMode变量的状态(表示是否立即改变大小)来决定是否调用AddPostPaint函数(这个函数很关键,后面讲)


         2、在UIEVENT_MOUSEMOVE事件里,LONG cx = event.ptMouse.x - ptLastMouse.x;计算出鼠标移动过程的偏移量,用来计算出控件移动的新位置,在事件处理的尾部的代码

					if( m_bImmMode ) {
						m_rcItem = m_rcNewPos;
						NeedParentUpdate();
					}
					else {
						rcInvalidate.Join(GetThumbRect(true));
						rcInvalidate.Join(GetThumbRect(false));
						if( m_pManager ) m_pManager->Invalidate(rcInvalidate);
					}

        根据 m_bImmMode来选择两种绘图的方式。一个是直接指定容器为新的区域大小,另一个是指定了无效区域,然后由DoPostPaint函数来完成绘制工作。


       3、在UIEVENT_BUTTONUP事件里,m_uButtonState变量取消为UISTATE_CAPTURED状态,并且强制刷新了界面。


分析完毕:


       在上面说到的内容里,立即改变容器大小很容易理解,我就不再说明了。我说一下非立即改变大小和位置,而是出现一个阴影效果的实现方式。这个效果更常用,比如我们在IM软件时,拖拽一个好友到另一个分组内,常常是有一个代表好友的阴影框跟着鼠标移动,可以通过这里的代码实现。


      要实现非立即改变大小和位置,必须用到让自己的控件重写DoPostPaint函数,并且在适当的是否调用PaintManager的AddPostPaint函数来注册自己,例如上面的例子在UIEVENT_BUTTONDOWN事件里调用AddPostPaint,在UIEVENT_BUTTONUP里调用RemovePostPaint。


      调用AddPostPaint后PaintManager就会在WM_PAINT消息里绘制完一次完整界面后,去调用我们的控件的DoPostPaint函数,让我们自己来完成绘制完之后的绘制!这时我们在DoPostPaint函数里完成阴影的绘制就可以了。如果不使用DuiLib的这个机制而直接在DoPaint函数里绘制阴影,就无法达到我们需要的效果,因为在DoPaint函数调用的时候软件的界面还可能没有完全绘制完,我们绘制的阴影可能会被其他控制的DoPaint函数覆盖!


       需要说明的是,在DoPosiPaint函数中,绘制的范围不仅仅是本控件的范围,而是整个程序的客户区,所以这个拖拽的阴影可以画到窗体的任意位置,当然我们也可以通过代码判断来控制绘制的范围,这也就是我之前说的duilib本身已经有支持拖拽的机制和条件了。


      看一下DoPostPaint的函数代码:

	void CHorizontalLayoutUI::DoPostPaint(HDC hDC, const RECT& rcPaint)
	{
		if( (m_uButtonState & UISTATE_CAPTURED) != 0 && !m_bImmMode ) {
			RECT rcSeparator = GetThumbRect(true);
			CRenderEngine::DrawColor(hDC, rcSeparator, 0xAA000000);
		}
	}

     函数里通过GetThumbRect函数获取了当前的分割条的位置,然后绘制了一个半透明的黑色阴影。讲到这里就已经完全说明了duilib中控件拖拽功能的实现方法了。


自己实现拖拽:

     我们同样可以有两种拖拽方案:

     一、立即拖拽模式:

    继承需要的控件并重写DoEvent函数,在UIEVENT_BUTTONDOWN、UIEVENT_BUTTONUP、UIEVENT_MOUSEMOVE三个事件里直接根据鼠标的位置来改变控件的坐标和大小,显然这就要求我们设置float属性为真,这种方式实现起来也简单,我就不写代码了。

     二、非立即拖拽模式:

     1、 继承需要的控件并重写DoEvent函数,在UIEVENT_BUTTONDOWN、UIEVENT_BUTTONUP、UIEVENT_MOUSEMOVE三个事件,重写DoPostPaint函数完成阴影的绘制

     2、在UIEVENT_BUTTONDOWN事件里调用AddPostPaint函数注册本控件

     3、在UIEVENT_MOUSEMOVE事件里计算新的控件位置,并且将新旧位置组合起来调用Invalidate函数刷新位置(否则会有残影)

    4、在UIEVENT_BUTTONUP事件里调用RemovePostPaint反注册自己,并且刷新控件。


    我写了个简单的例子,并且实验成功了,这是实验代码:

void CModulePaneCellUI::DoEvent(TEventUI& event)
{
	if( event.Type == UIEVENT_BUTTONDOWN && IsEnabled() )
	{
		if( ::PtInRect(&m_rcItem, event.ptMouse) ) 
		{
			m_uButtonState |= UISTATE_CAPTURED;
			m_ptLastMouse = event.ptMouse;
			m_rcNewPos = m_rcItem;

			if( m_pManager )
				m_pManager->AddPostPaint(this);
			return;
		}
	}
	if( event.Type == UIEVENT_BUTTONUP )
	{
		if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) 
		{
			m_uButtonState &= ~UISTATE_CAPTURED;
			CModulePaneConfigUI* pParent = static_cast<CModulePaneConfigUI*>(m_pParent);
			pParent->NotifyDrag(this);    // NotifyDrag函数是CModulePaneConfigUI容器的函数,和拖拽的效果本身没关系
			if(  m_pManager ) 
			{
				m_pManager->RemovePostPaint(this);
				m_pManager->Invalidate(m_rcNewPos);
			}
			NeedParentUpdate();
			return;
		}
	}
	if( event.Type == UIEVENT_MOUSEMOVE )
	{
		if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) 
		{
			LONG cx = event.ptMouse.x - m_ptLastMouse.x;
			LONG cy = event.ptMouse.y - m_ptLastMouse.y;

			m_ptLastMouse = event.ptMouse;

			RECT rcCurPos = m_rcNewPos;

			rcCurPos.left += cx;
			rcCurPos.right += cx;
			rcCurPos.top += cy;
			rcCurPos.bottom += cy;			

			//将当前拖拽块的位置 和 当前拖拽块的前一时刻的位置,刷新
			CDuiRect rcInvalidate = m_rcNewPos;
			m_rcNewPos = rcCurPos;
			rcInvalidate.Join(m_rcNewPos);
			if( m_pManager ) m_pManager->Invalidate(rcInvalidate);

			return;
		}
	}
	if( event.Type == UIEVENT_SETCURSOR )
	{
		if( IsEnabled() ) 
		{
			::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_HAND)));
			return;
		}
	}

	CLabelUI::DoEvent(event);
}

void CModulePaneCellUI::DoPostPaint(HDC hDC, const RECT& rcPaint)
{
	if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
		CDuiRect rcParent = m_pParent->GetPos();
		RECT rcUpdate ={0};
		rcUpdate.left = m_rcNewPos.left < rcParent.left ? rcParent.left : m_rcNewPos.left;
		rcUpdate.top = m_rcNewPos.top < rcParent.top ? rcParent.top : m_rcNewPos.top;
		rcUpdate.right = m_rcNewPos.right > rcParent.right ? rcParent.right : m_rcNewPos.right;
		rcUpdate.bottom = m_rcNewPos.bottom > rcParent.bottom ? rcParent.bottom : m_rcNewPos.bottom;
		CRenderEngine::DrawColor(hDC, rcUpdate, 0xAA000000);
	}
}

      这是实验效果,蓝色部分是原本的按钮控件,黑色阴影是正在被拖拽的过程。我在DoPostPaint中控制了阴影绘制范围不超过父容器。这只是实验,我没有用好看的素材去做,但是证明了可行性,替换素材后将会变得很好看。CModulePaneConfigUI是红色的容器,实现了拖拽元素的类似磁块效果自动布局,这个和拖拽效果本身没关系。CModulePaneCellUI是拖拽元素控件,他可以是继承任何Duilib控件而来的。





      使用这个方法,可以修改duilib原本的代码,为List控件和TreeView控件支持拖拽功能,也可以自定义某些特殊用途的控件。这里提供了绘制拖拽效果的思路,具体用法是多变的!


总结:

      这是我之前为了完成某个功能而写的代码,希望可以帮到需要的朋友。如果代码中有bug,或者有更好的拖拽实现方法,请联系我或者留言!


Redrain  2014.11.15


QQ:491646717


  

目录
相关文章
|
C# 容器
Winform控件优化之TabControl控件的美化和功能扩展
在基本的TabControl控件使用和功能之上,可以尝试对其进行美化和功能扩展,比如动态删除或添加tab、绘制图标按钮及鼠标hover时的背景变化、Tab从右向左布局的优化处理等。最重要...
2693 0
Winform控件优化之TabControl控件的美化和功能扩展
|
人工智能 搜索推荐 C#
Photoshop和WPF双剑配合,打造炫酷个性的进度条控件
结合Photoshop和WPF,共同创建一个矢量的个性化进度条。
551 0
Photoshop和WPF双剑配合,打造炫酷个性的进度条控件
|
XML 程序员 C语言
Qt编写控件属性设计器2-拖曳控件
一、前言 上一篇文章把插件加载好了,并且把插件中的所有控件都显示到了列表框中,这次要做的就是实现拖曳控件的功能,用户选择一个控件拖曳到画布上,松开,在松开位置处自动实例化该控件,这个需要用到dropEvent和dragEnterEvent事件,重新实现这两个事件,对拖曳的对象进行过滤并调用函数实例化该控件,在实例化该控件的同时实例化控件跟随控件以便拉伸调整大小和位置。
917 0
|
开发工具 C语言
Qt编写自定义控件40-导航进度条
一、前言 导航进度条控件,其实就是支付宝、京东、淘宝订单页面的进度控件,提示当前第几步,总共有几步,然后当前进度特殊颜色显示,每个进度带有时间文字等信息,本控件特意将三种样式风格都集成进去了,京东订单流程样式/淘宝订单流程样式/支付宝订单流程样式,可以动态切换样式,控件自适应任何分辨率,可以自由调整自身大小以适应分辨率的改变,总步骤以及当前步骤都是自动计算占用区域比例,直接提供接口设置步骤对应的文字信息等,接口非常友好。
1334 0
|
开发工具 C语言
Qt编写自定义控件9-导航按钮控件
一、前言 导航按钮控件,主要用于各种漂亮精美的导航条,我们经常在web中看到导航条都非常精美,都是html+css+js实现的,还自带动画过度效果,Qt提供的qss其实也是无敌的,支持基本上所有的CSS2属性,配合QPainter这个无敌大法工具,没有什么不能绘制的。
1275 0
一个控件几行代码实现换肤(可支持菜单)
这是从vbAccelerator(http://vbaccelerator.com)的皮肤程序修改而来,把DLL方式修改为OCX,并且修改为仅通过几行指令,即可完成皮肤加载。
512 0
|
C#
WPF一步步实现完全无边框自定义Window(附源码)
原文:WPF一步步实现完全无边框自定义Window(附源码)    在我们设计一个软件的时候,有很多时候我们需要按照美工的设计来重新设计整个版面,这当然包括主窗体,因为WPF为我们提供了强大的模板的特性,这就为我们自定义各种空间提供了可能性,这篇博客主要用来介绍如何自定义自己的Window,在介绍整个写作思路之前,我们来看看最终的效果。
1304 0
|
Web App开发 数据可视化 C#