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

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

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

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

分析机制:

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

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

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

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

  1. void CHorizontalLayoutUI::DoEvent(TEventUI& event)
  2. {
  3. if( m_iSepWidth != 0 ) {
  4. if( event.Type == UIEVENT_BUTTONDOWN && IsEnabled() )
  5. {
  6. RECT rcSeparator = GetThumbRect(false);
  7. if( ::PtInRect(&rcSeparator, event.ptMouse) ) {
  8. m_uButtonState |= UISTATE_CAPTURED;
  9. ptLastMouse = event.ptMouse;
  10. m_rcNewPos = m_rcItem;
  11. if( !m_bImmMode && m_pManager ) m_pManager->AddPostPaint(this);
  12. return;
  13. }
  14. }
  15. if( event.Type == UIEVENT_BUTTONUP )
  16. {
  17. if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
  18. m_uButtonState &= ~UISTATE_CAPTURED;
  19. m_rcItem = m_rcNewPos;
  20. if( !m_bImmMode && m_pManager ) m_pManager->RemovePostPaint(this);
  21. NeedParentUpdate();
  22. return;
  23. }
  24. }
  25. if( event.Type == UIEVENT_MOUSEMOVE )
  26. {
  27. if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
  28. LONG cx = event.ptMouse.x - ptLastMouse.x;
  29. ptLastMouse = event.ptMouse;
  30. RECT rc = m_rcNewPos;
  31. if( m_iSepWidth >= 0 ) {
  32. if( cx > 0 && event.ptMouse.x < m_rcNewPos.right - m_iSepWidth ) return;
  33. if( cx < 0 && event.ptMouse.x > m_rcNewPos.right ) return;
  34. rc.right += cx;
  35. if( rc.right - rc.left <= GetMinWidth() ) {
  36. if( m_rcNewPos.right - m_rcNewPos.left <= GetMinWidth() ) return;
  37. rc.right = rc.left + GetMinWidth();
  38. }
  39. if( rc.right - rc.left >= GetMaxWidth() ) {
  40. if( m_rcNewPos.right - m_rcNewPos.left >= GetMaxWidth() ) return;
  41. rc.right = rc.left + GetMaxWidth();
  42. }
  43. }
  44. else {
  45. if( cx > 0 && event.ptMouse.x < m_rcNewPos.left ) return;
  46. if( cx < 0 && event.ptMouse.x > m_rcNewPos.left - m_iSepWidth ) return;
  47. rc.left += cx;
  48. if( rc.right - rc.left <= GetMinWidth() ) {
  49. if( m_rcNewPos.right - m_rcNewPos.left <= GetMinWidth() ) return;
  50. rc.left = rc.right - GetMinWidth();
  51. }
  52. if( rc.right - rc.left >= GetMaxWidth() ) {
  53. if( m_rcNewPos.right - m_rcNewPos.left >= GetMaxWidth() ) return;
  54. rc.left = rc.right - GetMaxWidth();
  55. }
  56. }
  57.  
  58. CDuiRect rcInvalidate = GetThumbRect(true);
  59. m_rcNewPos = rc;
  60. m_cxyFixed.cx = m_rcNewPos.right - m_rcNewPos.left;
  61.  
  62. if( m_bImmMode ) {
  63. m_rcItem = m_rcNewPos;
  64. NeedParentUpdate();
  65. }
  66. else {
  67. rcInvalidate.Join(GetThumbRect(true));
  68. rcInvalidate.Join(GetThumbRect(false));
  69. if( m_pManager ) m_pManager->Invalidate(rcInvalidate);
  70. }
  71. return;
  72. }
  73. }
  74. if( event.Type == UIEVENT_SETCURSOR )
  75. {
  76. RECT rcSeparator = GetThumbRect(false);
  77. if( IsEnabled() && ::PtInRect(&rcSeparator, event.ptMouse) ) {
  78. ::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZEWE)));
  79. return;
  80. }
  81. }
  82. }
  83. CContainerUI::DoEvent(event);
  84. }

在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;计算出鼠标移动过程的偏移量,用来计算出控件移动的新位置,在事件处理的尾部的代码

  1. if( m_bImmMode ) {
  2. m_rcItem = m_rcNewPos;
  3. NeedParentUpdate();
  4. }
  5. else {
  6. rcInvalidate.Join(GetThumbRect(true));
  7. rcInvalidate.Join(GetThumbRect(false));
  8. if( m_pManager ) m_pManager->Invalidate(rcInvalidate);
  9. }

根据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的函数代码:

  1. void CHorizontalLayoutUI::DoPostPaint(HDC hDC, const RECT& rcPaint)
  2. {
  3. if( (m_uButtonState & UISTATE_CAPTURED) != 0 && !m_bImmMode ) {
  4. RECT rcSeparator = GetThumbRect(true);
  5. CRenderEngine::DrawColor(hDC, rcSeparator, 0xAA000000);
  6. }
  7. }

函数里通过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反注册自己,并且刷新控件。

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

  1. void CModulePaneCellUI::DoEvent(TEventUI& event)
  2. {
  3. if( event.Type == UIEVENT_BUTTONDOWN && IsEnabled() )
  4. {
  5. if( ::PtInRect(&m_rcItem, event.ptMouse) )
  6. {
  7. m_uButtonState |= UISTATE_CAPTURED;
  8. m_ptLastMouse = event.ptMouse;
  9. m_rcNewPos = m_rcItem;
  10.  
  11. if( m_pManager )
  12. m_pManager->AddPostPaint(this);
  13. return;
  14. }
  15. }
  16. if( event.Type == UIEVENT_BUTTONUP )
  17. {
  18. if( (m_uButtonState & UISTATE_CAPTURED) != 0 )
  19. {
  20. m_uButtonState &= ~UISTATE_CAPTURED;
  21. CModulePaneConfigUI* pParent = static_cast<CModulePaneConfigUI*>(m_pParent);
  22. pParent->NotifyDrag(this);
  23. if( m_pManager )
  24. {
  25. m_pManager->RemovePostPaint(this);
  26. m_pManager->Invalidate(m_rcNewPos);
  27. }
  28. NeedParentUpdate();
  29. return;
  30. }
  31. }
  32. if( event.Type == UIEVENT_MOUSEMOVE )
  33. {
  34. if( (m_uButtonState & UISTATE_CAPTURED) != 0 )
  35. {
  36. LONG cx = event.ptMouse.x - m_ptLastMouse.x;
  37. LONG cy = event.ptMouse.y - m_ptLastMouse.y;
  38.  
  39. m_ptLastMouse = event.ptMouse;
  40.  
  41. RECT rcCurPos = m_rcNewPos;
  42.  
  43. rcCurPos.left += cx;
  44. rcCurPos.right += cx;
  45. rcCurPos.top += cy;
  46. rcCurPos.bottom += cy;
  47.  
  48. //将当前拖拽块的位置 和 当前拖拽块的前一时刻的位置,刷新
  49. CDuiRect rcInvalidate = m_rcNewPos;
  50. m_rcNewPos = rcCurPos;
  51. rcInvalidate.Join(m_rcNewPos);
  52. if( m_pManager ) m_pManager->Invalidate(rcInvalidate);
  53.  
  54. return;
  55. }
  56. }
  57. if( event.Type == UIEVENT_SETCURSOR )
  58. {
  59. if( IsEnabled() )
  60. {
  61. ::SetCursor(::LoadCursor(NULL, MAKEINTRESOURCE(IDC_HAND)));
  62. return;
  63. }
  64. }
  65.  
  66. CLabelUI::DoEvent(event);
  67. }
  68.  
  69. void CModulePaneCellUI::DoPostPaint(HDC hDC, const RECT& rcPaint)
  70. {
  71. if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
  72. CDuiRect rcParent = m_pParent->GetPos();
  73. RECT rcUpdate ={0};
  74. rcUpdate.left = m_rcNewPos.left < rcParent.left ? rcParent.left : m_rcNewPos.left;
  75. rcUpdate.top = m_rcNewPos.top < rcParent.top ? rcParent.top : m_rcNewPos.top;
  76. rcUpdate.right = m_rcNewPos.right > rcParent.right ? rcParent.right : m_rcNewPos.right;
  77. rcUpdate.bottom = m_rcNewPos.bottom > rcParent.bottom ? rcParent.bottom : m_rcNewPos.bottom;
  78. CRenderEngine::DrawColor(hDC, rcUpdate, 0xAA000000);
  79. }
  80. }

这是实验效果,蓝色部分是原本的按钮控,件黑色阴影是正在被拖拽的过程。我在DoPostPaint中控制了阴影绘制范围不超过父容器。这只是实验,我没有用好看的素材去做,但是证明了可行性,替换素材后将会变得很好看:

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

总结:

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



Redrain  2014.11.15



QQ:491646717

duilib中控件拖拽功能的实现方法(附源码)的更多相关文章

  1. 仿酷狗音乐播放器开发日志二十三 修复Option控件显示状态不全的bug(附源码)

    转载请说明原出处,谢谢~~ 整个仿酷狗工程的开发将近尾声,现在还差选项设置窗体的部分,显然在设置窗体里用的最多的就是OptionUI控件,我在写好大致的布局后去测试效果,发现Option控件的显示效果 ...

  2. iOS涂色涂鸦效果、Swift仿喜马拉雅FM、抽屉转场动画、拖拽头像、标签选择器等源码

    iOS精选源码 LeeTagView 标签选择控件 为您的用户显示界面添加美观的加载视图 Swift4: 可拖动头像,增加物理属性 Swift版抽屉效果,自定义转场动画管理器 Swift 仿写喜马拉雅 ...

  3. C#:WebBrowser控件设置代理IP访问网站【附源码】

    软件截图 源码下载 http://download.csdn.net/detail/php_fly/8041731  

  4. RCP:拖拽功能的实现 Drag and Drop

    SWT中的拖拽是使用的org.eclipse.swt.dnd. 有三个需要密切注意的类: 1.DragSource 2.DropTarget 3.Transfer DragSource封装了需要被拖拽 ...

  5. PCB Winform中的WebBrowser扩展拖放(拖拽)功能 实现方法

    我们在Winform支持网页通常增加WebBrowser控件实现,相当于内嵌浏览器浏览网页使用, 而此WebBrowser默认情况是文件拖入功能是不支持的, 如何才能支持呢.在这里介绍如何实现方法 一 ...

  6. delphi的拖拽功能实现

    惭愧,编了这么多年程序,还没用过拖拽功能 这次同事要实现图标互换的功能,让我帮忙看一下,于是趁机研究了一下拖拽事件,发现还是比较简单的 参考了http://topic.csdn.net/u/20081 ...

  7. 给Winform中的TabControl添加更现代的拖拽功能

    上周接到一个开发任务,大致是允许APP中的Tab拖动以成为一个独立Tab,脱离之前的TabControl,就是现在Web拖动标签页创建新窗口的功能,现在浏览器必备的功能,应该很简单,然而我司采用的Do ...

  8. js实现登陆页面的拖拽功能

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>登 ...

  9. React Editor 应用编辑器(1) - 拖拽功能剖析

    这是可视化编辑器 Gaea-Editor 的第一篇连载分析文章,希望我能在有限的篇幅讲清楚制作这个网页编辑器的动机,以及可能带来的美好使用前景(画大饼).它会具有如下几个特征: 运行在网页 文档流布局 ...

随机推荐

  1. OPenGL中三维图形的矩阵变换

    对于二维的图形开发,拿简单的图片显示来说,我们主要的目的:就是在一块显示buffer中,不停的把每个像素进行着色,然后就可以绘制出来了.为了速度,很多其他的加速方法,但原理基本上就是这样了. 很直观, ...

  2. 【Linux高频命令专题(14)】nl

    概述 nl命令在linux系统中用来计算文件中行号.nl 可以将输出的文件内容自动的加上行号!其默认的结果与 cat -n 有点不太一样,nl 可以将行号做比较多的显示设计,包括位数与是否自动补齐 0 ...

  3. LR_问题_运行场景时提示scripts you are running in invalid

    问题描述 脚本在virtual user generator中运行正常. 在Controller中运行场景时报错: the target you defined cannot be reached. ...

  4. 4、JPA table主键生成策略(在JPA中table策略是首推!!!)

    用 table 来生成主键详解 它是在不影响性能情况下,通用性最强的 JPA 主键生成器.这种方法生成主键的策略可以适用于任何数据库,不必担心不同数据库不兼容造成的问题. initialValue不起 ...

  5. C#基础(四)

                                                          语句          到目前为止,我们的程序还只能按照编写的顺序执行,中途不能发生任何变化 ...

  6. ios 使用GCD 多线程 教程

    什么是GCD Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法.该方法在Mac OS X 10.6雪豹中首次推出,并随后被引入到了iOS4.0中.GCD ...

  7. MySQL5.7表空间加密

    MySQL5.7开始支持表空间加密了,增强了MySQL的数据文件的安全性,这是一个很不错的一个功能,这个特性默认是没有启用的,要使用这个功能要安装插件keyring_file. 下面就来看看怎么安装, ...

  8. Android Studio Gradle

    http://blog.zhaiyifan.cn/2016/03/14/android-new-project-from-0-p2/

  9. Python中模拟enum枚举类型的5种方法分享

    这篇文章主要介绍了Python中模拟enum枚举类型的5种方法分享,本文直接给出实现代码,需要的朋友可以参考下   以下几种方法来模拟enum:(感觉方法一简单实用) 复制代码代码如下: # way1 ...

  10. bzoj4044

    这题简直了………… 首先根据操作可知,我们肯定是先造出某个偶数长度的回文串,然后添加若干字符得到设回文串长为len[x] 则ans=min(n-len[x]+f[x]); 那么问题就是制造这个串的回文 ...