也谈自己实现菜单
2 Comments 2019-05-20 admin
Windows的菜单我们每天都在使用,一般情况下,如果需要美化菜单,例如改变字体、背景颜色等,都是直接在菜单的AdvancedDrawItem事件里面进行自绘即可。但如果想实现一些特殊效果,比如说菜单半透明,就的自己实现整个菜单了。
实现一个菜单大概需要处理这些问题:(1)绘制各个菜单项。(2)菜单窗口弹出来后,所属窗口不应该失去焦点。(3)点击了菜单项后应该关闭菜单。对于问题1,我们可以简单的根据菜单项的数量,然后判断状态(比如说鼠标现在是否位于该项目上面)进行绘制;对于问题3,需要判断鼠标点击事件。对于问题2,我们可以类似实现自定义Combobox一样,响应弹出窗口的WM_ACTIVATE消息:
procedure TFrmMenu.WMActivate(var Message: TWMActivate); begin inherited; if Message.Active = Integer(False) then begin if Visible then begin Close; end; end else begin SendMessage(FrmParent.Handle, WM_NCACTIVATE, 1, 0); end; end;
就这样,我们第一个版本出炉了。点击这里下载源码。
如果我们仔细观察,会发现这个菜单跟系统的菜单还是有区别的,点击按钮弹出我们的菜单,然后在窗口标题栏右键弹出系统菜单来对比,会发现以下的不同(弹出我们的菜单后,不要关闭):(1)移动鼠标到其它控件(比如说Panel),会触发该控件的鼠标进入CM_MOUSEEnter和鼠标离开CM_MOUSELEAVE消息。移动到窗口标题按钮也是一样(触发NC消息)。(2)移动鼠标到输入框,比如说TMemo上,会触发对方的WM_SETCURSOR消息,从而改变鼠标形状。怎么办呢?通过搜索,找到这篇文章《DirectUI中模态对话框和菜单的原理(自己控制整个Windows消息循环。或者,用菜单模拟窗体打开时用SetCapture取得控制权,一旦窗体收到WM_CAPTURECHANGED消息就把窗体退出)》:
这篇文章介绍的是SDK方式的C代码实现代码,但是后面有一句:“我的做法是菜单模拟窗体打开时用SetCapture取得控制权,一旦窗体收到WM_CAPTURECHANGED消息就把窗体退出。”,于是我们简单修改一下,便有了第二个版本。点击这里下载源码。
我们再仔细观察,模态窗口解决了版本1的问题(1)和问题(2)—实际上问题1只解决了一半,如果移动鼠标到窗口标题按钮上,还是会触发消息的。最重要的是,版本二带来了一个新问题:弹出菜单后,不要关闭,直接点击其它控件(比如说按钮1或标题栏最大化按钮),发现没有触发点击事件,而是菜单关闭而已。于是我们再搜索,又发现一篇跟前面的文章差不多的另外一篇:新版MenuDemo——使用Duilib模拟Windows原生菜单于是我们把它翻译成Delphi,并完善了一下(比如说该VC版本的源码没有处理WM_MOUSEACTIVATE消息,在菜单关闭后所属窗口会因为获得焦点闪烁一下)。第三个版本出炉:点击这里下载源码。
通过运行第三个版本,我们发现好像又回到了版本1:(1)移动鼠标到窗口标题按钮(触发NC消息)。(2)移动鼠标到输入框,比如说TMemo上,会触发对方的WM_SETCURSOR消息,从而改变鼠标形状。
抽完一支烟后,我们决定在版本三的基础上,加上类似版本二的模态窗口功能,但又要解决版本二的问题:(1)在消息循环开始前,我们使用API SetCapture(m_hWindow);来实现类似的模态窗口,在菜单关闭后使用ReleaseCapture;还原。(2)对于NC消息和客户区点击消息,我们在消息循环里面转发:
原文的代码:
else if (msg.message = WM_LBUTTONDOWN) or (msg.message = WM_RBUTTONDOWN) or (msg.message = WM_NCLBUTTONDOWN) or (msg.message = WM_NCRBUTTONDOWN) or (msg.message = WM_LBUTTONDBLCLK) then begin //click on other window if not IsMenuWnd(msg.hwnd) then begin DestroyMenu; //为了和菜单再次的弹出消息同步 PostMessage(msg.hwnd, msg.message, msg.wParam, msg.lParam); bInterceptOther := True; bMenuDestroyed := True; end; end
修改后的代码:
else if (msg.message = WM_LBUTTONDOWN) or (msg.message = WM_RBUTTONDOWN) or (msg.message = WM_NCLBUTTONDOWN) or (msg.message = WM_NCRBUTTONDOWN) or (msg.message = WM_LBUTTONDBLCLK) then begin h := WindowFromPoint(Mouse.CursorPos); //判断鼠标点击的窗口 if h <> m_hWindow then begin DestroyMenu; msg.hwnd := h; if h = m_hWndOwner then //点击的菜单所属窗口 begin { 判断点击的是什么地方,用于发送WM_MOUSEMOVE,从而触发WM_SETCURSOR。 可以作如下测试: 菜单弹出后,不要关闭,然后鼠标移动到四个角上面,或者边框上面, 点下鼠标按钮不要松开,这时候,菜单关闭,然后鼠标形状变成窗口可改变 大小形状,移动鼠标即可改变窗口大小。 如果不处理,则会出现鼠标形状改变,但拖动鼠标不会改变大小(可以注释掉 下面的 SendMessage(m_hWndOwner, WM_SETCURSOR, m_hWndOwner, MakeLong(xRet, WM_MOUSEMOVE)); 来对比效果。 } xRet := SendMessage(m_hWndOwner, WM_NCHITTEST, 0, MakeLong(Mouse.CursorPos.X, Mouse.CursorPos.Y)); if xRet <> HTCLIENT then begin SendMessage(m_hWndOwner, WM_SETCURSOR, m_hWndOwner, MakeLong(xRet, WM_MOUSEMOVE)); //边框鼠标 //下面两个消息是为了响应菜单未关闭时点击标题栏按钮 case msg.message of WM_LBUTTONDOWN: msg.message := WM_NCLBUTTONDOWN; WM_RBUTTONDOWN: msg.message := WM_NCRBUTTONDOWN; end; msg.wParam := xRet; msg.lParam := MakeLong(Mouse.CursorPos.X, Mouse.CursorPos.Y); end; end; //可能点击的是窗口上面的按钮 PostMessage(msg.hwnd, msg.message, msg.wParam, msg.lParam); if IsMenuWnd(msg.hwnd) then nRet := m_nSelectIndex; DestroyMenu; PostMessage(msg.hwnd, msg.message, msg.wParam, msg.lParam); bInterceptOther := True; bMenuDestroyed := True; end; end
点击这里下载源码。
现在看起来,我们的菜单好像跟操作系统的一模一样了。但是,实际上,如果再仔细观察,菜单弹出后,直接双击窗口标题栏来最大化窗口,或双击窗口左上角图标来关闭窗口,没有达到预期效果。当然,迅雷和QQ之类的自定义菜单也是不够“完美”的,最多也就是版本三左右的效果:

估计原理都是跟上面引用的文章一样。但是也不排除人家想要的就是这种所谓不完美的效果。
如何解决这个问题呢?从现象上看,应该是跟鼠标双击消息有关。我们是否类似上面单击一样,把消息WM_LBUTTONDBLCLK转化为WM_NCLBUTTONDBLCLK即可解决问题?回头看看上面的代码:
else if (msg.message = WM_LBUTTONDOWN) or (msg.message = WM_RBUTTONDOWN) or (msg.message = WM_NCLBUTTONDOWN) or (msg.message = WM_NCRBUTTONDOWN) or (msg.message = WM_LBUTTONDBLCLK) then begin if (msg.message = WM_LBUTTONDBLCLK) then ShowMessage('触发了鼠标双击事件!');//加上这一句 ...... end
原来我们实在是太天真了,这个消息压根就没触发。网上搜索,找到这篇文章:Windows如何区分鼠标双击和两次单击,上面说:“两次单击会产生四个鼠标点击消息,如果第三个消息(第二次按下)和第二个消息(第一次弹起引发的WM_LBUTTONUP)间隔短于指定值,则把第三个消息处理成WM_LBUTTONDBLCLK消息;第四个消息照旧,WM_LBUTTONUP。”。通过打印消息,发现我们的程序的确触发了两次WM_LBUTTONDOWN,而且时间间隔、点击坐标都符合条件。拿出Spy++,我们对比一下弹出系统标题栏菜单后直接双击标题栏,和弹出我们的菜单后直接双击标题栏,通过消息对比,发现收到的消息类型、顺序都是一模一样的,但系统的,第二个WM_LBUTTONDOWN后,会产生一个WM_LBUTTONDBLCLK,而我们的自定义菜单就是不会出现这个消息。
现在我们只能猜想,有可能是操作系统还有什么消息发送给了菜单窗口,但Spy++没有截获?最后,我们再回到第三个版本,把代码进一步精简,就是只处理我们需要的消息即可。关键代码是:
//if PeekMessage(msg, 0, 0, 0, PM_REMOVE) then if PeekMessage(msg, 0, 0, 0, PM_NOYIELD or PM_NOREMOVE) then//不删除,下面判断是属于需要处理的,我们才从消息队列删除并处理
最终,得到完美的第五个版本。点击这里下载。
分类:界面设计
2 Comments 发表评论
鬼使神差的打开了老陈的的网站,打开后自己都感觉莫名其妙,岁数大了念旧了,怀念和你聊天的日子,怀念藏鲸阁
还以为你不更新记录了。哈哈哈
有空联系
发表评论
XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
TrackBack URL | RSS feed for comments on this post.