转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/38253297
第一部分
我在前一段时间研究了怎么制作duilib的菜单,花了几天时间以MenuDemo为基础做出个duilib的菜单以备自用,近些天在群里经常会碰到群友问如何给MenuDemo增加消息响应,为了避免重复的回答我特意写这篇日志,希望可以帮到需要之人,同时也介绍了如何美化菜单的效果、动态修改自身的状态以及通过增加属性来优化菜单的xml文件编写过程,先截个图展示一下效果。
本菜单的优点如下:
1、可以展现多级菜单
2、可内嵌自定义控件,并且控件可以向主窗体发送消息,如图的红色叹号就是个按钮控件,可以制作酷狗音乐的托盘菜单的播放暂停按钮和进度控制进度条。
3、菜单拥有阴影效果
4、菜单可以自定义前方显示小图标,并且可以控制图标的大小和是否显示
5、菜单可以根据是否拥有子菜单决定是否显示小箭头
6、菜单可以添加分割线
7、每个菜单项都可以实现check菜单的功能,而且check信息会被保存下来以备下次显示
8、优化菜单的xml描述文件,编写方便容易,如果要写一个二级菜单,比如编写图片中的菜单测试4以及他的子菜单,只需如下代码就可以了
9、可以通过键盘的按钮控制菜单的选项
10、每个菜单项的高度宽度是任意调整的
<MenuElement text="菜单测试4" icon="right.png" iconsize="9,9" expland="true" > <MenuElement text="菜单测试5" expland="true" icon="WebSit.png" iconsize="16,16"> <MenuElement text="菜单测试6" icon="Virus.png" iconsize="16,16"/> <MenuElement text="菜单测试7" /> </MenuElement> <MenuElement text="菜单测试8" expland="true"> <MenuElement text="菜单测试a" /> <MenuElement text="菜单测试b" /> </MenuElement> </MenuElement>
第二部分
菜单介绍完了,先说说我的开发过程的准备阶段。
大家知道duilib库是个非常棒的开源库,我个人很喜欢他,但是由于现在作者不维护了,导致现在库中存在的bug无人料理,控件也相对不足,而菜单控件是软件开发中常用的,duilib库中虽然给出了MenuDemo,但是没有增加消息响应,导致一些朋友不会使用他,我开始也是这样,所以打算自己给duilib做一个菜单。
我接触到的和duilib有关的菜单的例子有:原带的MenuDemo、Alberl写的简易Menu、ListDemo中的MenuWnd、其他库的已经编写成熟的Menu.
我找了很多Menu的代码,看了看他们的实现方法。我在这里简单的说一下他们的优缺点:
原带的MenuDemo:
优点:1、实现了多级菜单
2、内嵌自定义控件
缺点:1、无消息响应功能
2、菜单的xml描述文件过于繁琐,在其中实现小箭头或者小图标都要靠自己的布局来实现,这点大家可以看他的xml文件。
Alberl写的简易Menu和ListDemo中的MenuWnd:
优点:1、使用简单方便
2、外观效果很好
缺点:1、只是一级菜单,无法满足很多需要多级菜单的场合
2、没有封装为菜单类,无法通过调节属性来控制菜单的效果
3、菜单的逻辑和消息处理比较简单,逻辑效果不够完善
总结了他们的优缺点之后,我打算做出个更通用的菜单,更简单,效果更好的菜单,他拥有多级菜单、内嵌自定义控件、可以实现消息响应、外
观效果要好、可以通过简单的xml文件描述就把菜单编写出来、可以显示小图标和小箭头、菜单内实现check的功能,并且可以把是否被check的信息
保存下来。
第三部分
现在说一下我的编写过程,开始我想自己写一个菜单,但是我写到一半的时候就被难倒了,由于我个人水平的限制。多级菜单的实现我一直无法
完美搞定,看似简单的东西,其实里面包含很多逻辑判断:多级菜单的显示位置,显示大小,如何通知上级菜单自己被销毁,怎么发送通知,何时销毁
自己,应该显示那个下级菜单,怎么保存子菜单的信息。实在搞不下去我只好去求救于现成的MenuDemo,他的最大优点就是已经写好的多级菜单的
逻辑,而且并不需要再修改这部分逻辑,而这正是我认为最难实现的部分,所以我就直接在MenuDemo的基础上进行下一步开发,作者用观察者模式
很好的实现了这个功能,前人栽树后人乘凉,在此谢过编写MenuDemo的作者。
接下在我先吧多数人最关心的怎么发送并响应菜单的消息的部分说明一下。
要实现消息响应有三种方法:1、通过给菜单增加Notify接口实现 2、通过给菜单项增加委托实现 3、通过系统的消息机制实现
实际上第一种和第二种的方法是类似的,而第三种方法最麻烦但是效果和性能是做好的。
我先介绍第一种和第二种方法
通过读MenuDemo的代码可以知道,菜单实际上是一个弹出窗体,既然我们的主窗体可以响应控件的消息,那么这个弹出窗体同样可以实现,我
们要做到就是在菜单类CMenuWnd中继承消息通知接口INotifyUI,这样PaintManager类就会在合适的时候把消息发送到继承了这个接口的窗体上,我
们要在CMenuWnd类中实现Notify函数,这个大家应该很熟悉,然后我们在CMenuWnd类的HandleMessage函数的WM_CREATE消息处理中增加这个
代码,m_pm.AttachDialog(pRoot);(如果不知道在哪里添加,可以搜索语句m_pm.AttachDialog(pRoot);,在这句代码下添加,记住有两处需要添
加),增加了这个代码后PaintManager类在发现消息后就会把这个消息分发到拥有Notify函数的窗体中,这样我们就能在CMenuWnd类的Notify函数中
接收到菜单被单击的消息了,这个处理方法大家应该很熟悉的。(PS:ListDemo就是用这个方法来处理消息,如果还不会的朋友可以看一下他的
代码)
第二种方法是通过委托,这是模仿c#的功能,实际上也就是通过巧妙的回调函数实现的技术。当我们创建了菜单后,在CMenuUI类的Add和AddAt
函数中增加类似这样的代码:
pMenuItem->OnNotify += MakeDelegate(this, &CMenuUI::OnMenuElemrntClick);
然后我们在CMenuUI类中增加这种形式的函数bool Fn(void*);比如例子中的函数为bool OnMenuElemrntClick(void* prarm);函数体如下
bool OnMenuElemrntClick(void* param) { TNotifyUI* pMsg = (TNotifyUI*)param; if( pMsg->sType == _T("itemclick")) { //处理消息 } return true; }
这样就能接收到消息,接收到消息后就是如何把消息发送到主窗体让他来处理消息,为此我们要在主窗体创建菜单时把任意一个控件的指针传到
菜单里面,然后菜单里保存这个指针,比如为m_pOwner,然后在Notify函数或者OnMenuElemrntClick函数里,首先判断出是单击了那个菜单项,然后
使用如下代码把消息发送到主窗体:
if( m_pOwner ) m_pOwner->GetManager()->SendNotify(m_pOwner, _T("菜单消息"), 0, 0, true);
回到主窗体,在主窗体的Notify函数里就可以接收到这个消息了,接收的demo如下
if( msg.sType == _T("菜单消息") ) { //收到了菜单的消息 }
至此,第一种和第二种方法就介绍完了,我最开始就是使用这个方法发送消息的,但是注意这个方法有缺陷:不能在主窗体的接收函数中做出
会导致菜单销毁的事情,比如最常见的弹出Messagebox或者模式窗体,否则程序会崩溃。原因很简单,因为首先是菜单发送消息给主窗体,然
后主窗体收到消息,但是注意这是菜单还有剩余代码没有执行完,如果这时弹出模式窗体,菜单就会因为失去焦点而销毁,等到弹出的模式窗体销毁
后,程序会执行菜单里没有执行完的代码,但是此时菜单已经被销毁,所以程序就崩溃了,切记!
第三种方法
这是我现在使用的也是推荐给大家的方法,就是使用系统的消息机制来将菜单项的单击发送给主窗体,这个方法快速有效,而且没有第一种和第
二种方法的缺陷。
首先我们在菜单类的头文件中自定义一个小,比如我的是
#define WM_MENUCLICK WM_USER +121 //用来接收按钮单击的消息
然后我们在CMenuElementUI类的每一个响应单击的位置加上这句代码
PostMessage(s_context_menu_observer.m_hMainWnd, WM_MENUCLICK, (WPARAM)(new CDuiString(GetName().GetData())), (LPARAM)0);
这样就把消息发送到主窗体,为此我们需要在菜单建立时把主窗体的句柄传进来并且保存,我把这个句柄保存到了s_context_menu_observer,
因为他是原demo自带的一个全局变量,很好的满足 了我们的需求。这里注意我是把菜单的Name传送了过去而不是传送ID,这符合duilib的使用习惯。
注意传送字符串我使用了new,在处理了字符串后一定要记得delete他,否则会内存泄漏!
接着我们在主窗体里接收这个消息,只要我们的窗体是继承自WindowImplBase类,我们就可以在窗体类中重写HandleCustomMessage函数,这
个函数本意就是让我们来操作自定义消息的,如果不是继承自WindowImplBase类,那就直接在HanddlMessage函数里响应了。收到了这个消息,想怎
么处理都随你了。
至此,我就介绍完了最关键的怎么响应消息的部分,我做得那个菜单同时使用了第一种和第三种方法,用第一种方法来发送自动一控件的消息,
如播放被单击;用第三种方法来传递菜单项的单击消息,这样菜单的消息传送就完全了。
第四部分
优化代码和显示效果
可以看到MenuDemo的xml文件的编写实在是复杂难懂,他只是提供了个容器,里面显示的内容完全要靠自己的布局来完成,这样过于繁琐,所以
我通过增加属性的方法简化了这个过程,可以在文章的篇头看到我的那个xml代码比原demo的xml少多了。
为了减少代码量,我给菜单项增加了icon、 iconsize、ischeck、expland四个属性,分别控制显示的小图标,图标的大小、是否被选中,是否可以
展开。然后在类中增加了相应的函数,用来绘制小图标,扩展图标,绘制的时候要根据自己所在的菜单项的高度宽度和自己的大小来调整合适的位置,
还有改不改绘制出来。这样子就简化了xml的编写,不过说起来简单,我做的时候在处理小图标和扩展图标的显示的先后顺序上也花了点时间,估计是
自己的逻辑还不够清晰。
另外我还增加了一个属性"type",当属性值为"line"时, 就绘制一条横线,绘制的横线的颜色、宽度等信息是从菜单的Default标签中获取的。
接着为了给菜单增加阴影效果,我使用了CShadowWnd类,这个类的用法和demo我在多个duilib群里都上传过,就不再赘述了。
最后再说一下动态修改自己的状态,为了给菜单增加check的功能,我们要保存下来每个菜单项的check状态,而菜单每次都是重新创建的,所以
这个状态信息不能在菜单本身保存,我在主窗体中声明一个map<CDuiString,bool>模版的变量用来保存菜单项的状态,第一个字段保存菜单的名称,
用来区别不同的菜单项,第二个字段来区分菜单是否被选中,创建菜单时这个变量传进去,然后让菜单自己来维护这个变量,当菜单项被单击时,他会
从变量中找到自己并且修改为应有的值,菜单在创建时会读取这个变量找到自己的信息来决定是否绘制小图标。
结束语
至此,我的这篇日志就结束了,我大致把自己实现这个菜单的思路讲了一下,但是由于我的个人水平的原因,可能代码逻辑并不好,我的说明也
不够清晰。如果你有什么建议,非常欢迎给我提出来,可以加我QQ,也可以留言。现在的菜单控件我已经重构,并且上传了源码。
附上窗体阴影的代码和Demo:点击打开链接
此菜单控件最新的代码和使用demo已经开源,详见开源博文 :点击打开链接
2014.7.29 10:41 Redrain