新版MenuDemo——使用Duilib模拟Windows原生菜单

来源:互联网 发布:unity3d dontdestroy 编辑:程序博客网 时间:2024/06/04 18:19

原文链接http://blog.csdn.net/Skilla/article/details/42078687

        相信玩Duilib的朋友已经期待这篇文章很久了,因为我在一周前发表的文章——“无焦点窗口的实现”里面提到了无焦点窗口在菜单里面的应用,并承诺大家,写一个关于Menu实现的Demo分享给大家。先上几张截图,看一下效果


          怎么样,Skilla这次的作品还能让你心动吧,没错,上面的菜单效果可不是酷狗里面的截图,就是我们熟悉的Duilib实现的。

         说起菜单,我们都想起了原版的MenuDemo,凡是阅读过原版MenuDemo的朋友,应该很清楚它的原理,它是利用焦点来控制菜单何销毁。焦点的介入给级联菜单的维护造增添了不少难度,而且无谓的焦点切换也是一种浪费,还有就是键盘事件如何加入也是一个很棘手的问题。这次Skilla提供的MenuDemo是无焦点的,使它用起来更加自然,更加原生态。关于DirectUI的菜单,我们把重点放在“模拟”两个字上,在菜单实现时,不但要模拟出表象,更要模拟出本质。我们在拿窗口去模拟菜单时,一定要先摸Windows原生菜单实现的内部原理,否则会走很多弯路。下面先介绍一下Windows原生菜单的机制,先上一段代码。


[cpp] view plaincopy
  1. int CDuiMenu::RunMenu()  
  2. {  
  3.       
  4.     int nRet(-1);  
  5.     BOOL bMenuDestroyed(FALSE);  
  6.     BOOL bMsgQuit(FALSE);  
  7.     while(TRUE)  
  8.     {  
  9.   
  10.         if(m_menuRet.bExit)  
  11.         {  
  12.             nRet = 0;  
  13.             break;  
  14.         }  
  15.   
  16.         if(GetForegroundWindow() != m_hWndOwner)  
  17.         {  
  18.             break;  
  19.         }  
  20.         BOOL bInterceptOther(FALSE);  
  21.         MSG msg = {0};  
  22.         if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))  
  23.         {  
  24.             if(msg.message == WM_KEYDOWN  
  25.                 || msg.message == WM_SYSKEYDOWN  
  26.                 || msg.message == WM_KEYUP  
  27.                 || msg.message == WM_SYSKEYUP  
  28.                 || msg.message == WM_CHAR  
  29.                 || msg.message == WM_IME_CHAR)  
  30.             {  
  31.                 //transfer the message to menu window  
  32.                 if (m_menus->IsKeyEvent())  
  33.                 {  
  34.                     SKILL_ASSERT(m_pKeyEvent->GetMenuWnd()->GetHWND());  
  35.                     msg.hwnd = m_pKeyEvent->GetMenuWnd()->GetHWND();  
  36.                 }  
  37.             }  
  38.             else if(msg.message == WM_LBUTTONDOWN  
  39.                 || msg.message  == WM_RBUTTONDOWN  
  40.                 || msg.message  == WM_NCLBUTTONDOWN  
  41.                 || msg.message  == WM_NCRBUTTONDOWN  
  42.                 ||msg.message   ==WM_LBUTTONDBLCLK)  
  43.             {  
  44.   
  45.   
  46.                 //click on other window  
  47.                 if(!IsMenuWnd(msg.hwnd))  
  48.                 {  
  49.                     DestroyMenu();  
  50.                     //为了和菜单再次的弹出消息同步  
  51.                     ::PostMessage(msg.hwnd,msg.message,msg.wParam,msg.lParam);  
  52.                     bInterceptOther = true;  
  53.                     bMenuDestroyed = TRUE;  
  54.   
  55.                 }  
  56.             }else if (msg.message == WM_LBUTTONUP  
  57.                 ||msg.message==WM_RBUTTONUP  
  58.                 ||msg.message==WM_NCLBUTTONUP  
  59.                 ||msg.message==WM_NCRBUTTONUP  
  60.                 ||msg.message==WM_CONTEXTMENU)  
  61.             {  
  62.                 if(!IsMenuWnd(msg.hwnd))  
  63.                 {  
  64.                     //防止菜单同时弹出多个  
  65.                     ::PostMessage(msg.hwnd,msg.message,msg.wParam,msg.lParam);  
  66.                     break;  
  67.   
  68.                 }  
  69.   
  70.             }  
  71.             else if(msg.message == WM_QUIT)  
  72.             {  
  73.   
  74.                 bMsgQuit = TRUE;  
  75.             }  
  76.   
  77.             //拦截非菜单窗口的MouseMove消息  
  78.             if (msg.message == WM_MOUSEMOVE)  
  79.             {  
  80.                 if (!IsMenuWnd(msg.hwnd))  
  81.                 {  
  82.                     bInterceptOther=TRUE;  
  83.                 }  
  84.             }  
  85.   
  86.   
  87.   
  88.   
  89.             if (!bInterceptOther)  
  90.             {  
  91.                 TranslateMessage (&msg);  
  92.                 DispatchMessage (&msg);  
  93.             }  
  94.   
  95.         }  
  96.         else  
  97.         {  
  98.             MsgWaitForMultipleObjects (0, 0, 0, 10, QS_ALLINPUT);  
  99.         }  
  100.   
  101.         if(bMenuDestroyed) break;  
  102.   
  103.         if(bMsgQuit)  
  104.         {  
  105.             PostQuitMessage(msg.wParam);  
  106.             break;  
  107.         }  
  108.     }  
  109.   
  110.     if(!bMenuDestroyed)  
  111.     {  
  112.         DestroyMenu();  
  113.     }  
  114.     return nRet;  
  115. }  

             我们都知道,焦点时键盘消息的“舞台”,要说菜单没有焦点,那么我们在按键盘上的,VK_UP,VK_DOWN,VK_LEFT,VK_RIGHT,VK_RETURN时,为什么菜单会有反应?可能你还不信,菜单在弹出时,内部是阻塞模式的,它会像DoModel一样,在里面开启一个while循环,这样我们就可以随心所欲地控制消息走向了。当菜单弹出时,消息循环建立,只要是该进程的消息,不管是哪个窗口的都会到这里来,当收到键盘类消息时,不管是哪个窗口的,都分配给菜单窗口;当鼠标键按下时,判断是不是菜单窗口的,只要不是,菜单销毁,把从消息队列取出的消息再次扔给消息队列,循环退出;鼠标键弹起时,只要不是菜单窗口的,取出消息,扔回消息队列(就像漂流瓶一样,捞上来了发现不需要又给扔回去,其实这样做的目的是为了,给菜单本次的销毁和下次再次弹出做一个过渡,否则上次的还没有销毁,这次的就弹出,就乱套了);当有MouseMove消息时,只要不是菜单窗口的,通通拦下,因为菜单弹出时,其他窗口的MouseMove事件会屏蔽掉,不信看一下Windows的原生菜单是不是有这个效果;当Owner窗口从前台切换到后台时,菜单同样要销毁,循环退出;当MenuItem收到点击事件时,获取菜单命令Id,并通知菜单的消息循环结束,返回菜单命令Id并转发给Owner窗口WM_COMMAD消息。

            怎么样,这样一分析,菜单的实现是不是变得豁然开朗了?在实现级联菜单时再也不用考虑焦点的问题了,键盘事件的加入也变得易如反掌了。菜单效果实现的好固然重要,但更重要的是要好用。封装的好用不要用就看Duilib给不给力啦,经Skilla测试Duilib还是非常给力的,哈哈,开个玩笑。Duilib里面的CContainerUI是个控件容器,作为显示使用的,我们就利用CContainerUI做我们的菜单容器,只不过不直接显示了,我们就当做一个普通的数组来使用,为什么呢?当然是因为解析方便了。因为有级联的需求,菜单自然不是一个,我们就用CContainerUI作为我们的容器把,用CDialogBuilder解析出来,自然就是一个菜单数组了。这是菜单解析xml时的最外层的tag,为了给它加两个属性,我们继承CContainerUI,重新弄一个MenuContainer,重写SetAttribute,加两个属性,一个是interval(两级菜单的间隔,如果不明白,自己修改试试),另一个是keyevent这是一个bool属性(是否开启键盘)。

          MenuContainer里面就是我们的一个一个的菜单了,为了方便绘制,我们使用CListUI作为菜单的UI,以CListUI为基类派生CMenuUI,我们又可以重写SetAttribute给它加入我们需要的属性,这里我还是给他添加了两个属性,一个major(bool类型,是否为主菜单),一个bktrans(bool类型,是否开启透明),其他属性沿用CListUI的属性。

         CMenuUI里面就是一个一个的菜单项了,为了满足各种需求,Skilla设计了两种菜单项,一种叫做MenuItem(普通菜单项),另一种叫做StaticMenuItem(静态菜单项),顾名思义,MenuItem是这真正的菜单项,鼠标指上去会有悬浮事件,按键盘上下,能够选中的菜单项;StaticMenuItem就是那些和菜单无关的控件,比如菜单分隔线,酷狗任务栏菜单上面的播放按钮,滑动条等。不偏不倚,Skilla同样赋予了MenuItem两个属性,一个comand(字符串,菜单消息循环退出时的返回值,这是菜单命令响应的依据),另一个extendmenu(字符串,子菜单的name,这个是各个菜单级联的关键)。

          使用本次的MenuUI只需引入两个文件,一个MenuUI.h和一个MenuUI.cpp。还有需要简单地修改一下源码,1.按照skilla的上一篇文章,修改CPaintManagerUI,让它支持无焦点窗口   2.修改AttachDialog,因为CPaintManagerUI在析构时会把绑定到上面的控件数也析构掉,因为Menu的所有控件都是我们自己调用CDialogBuilder创建的,自然要我们自己销毁,所以为了不让 CPaintManagerUI干涉,我们加一个bool m_bAutoDeleteControls属性,并初始化为false。给AttachDialog加一个bool参数,默认值为true,函数体内赋值给m_bAutoDeleteControls,最后给CPaintManagerUI的析构函数里面的delete m_pRoot加上一个if (m_bAutoDeleteControls)判断。修改完以后,引入MenuUI.h和一个MenuUI.cpp这两个文件就可以使用了。

         最后谈一下Skilla对Windows界面编程的一些看法,现在开源的界面库虽然很多,随便拿出一个来就能做出非常炫的界面。但是,我们不能过分迷恋任何界面库。既然玩的是Windows就不能忘本,你用的是Duilib也好,SOUI也罢,都需要仔细去揣摩Windows API,因为我们的任何控件都是在模拟。还是那句话,没有哪款界面库是完全符合所有需求的,要想尽善尽美,还得自己动手。


   不好意思,由于上传之前没有严格测试,上次上传的Release版本存在bug(原因是由于,添加SetUnfocusPaintWindow函数时,函数体写在头文件了,编译成Release库后出现了问题)。现在已经修改并重新上传了一份,需要的朋友前来下载。

          源码及Demo下载(如果有建议或者发现bug请直接留言,或者联系QQ:848861075(Skilla)谢谢!)

        

        



0 0
原创粉丝点击