C Q&A 专栏...
MFC 应用程序中的菜单提示信息
原著:Paul DiLascia
翻译:
下载源代码:(193KB)
原文出处:(MSDN Magazine
November 2003)
我正在做一个历时很久的项目。出于某些原因,项目启动之初我们实现了自己的弹出式菜单。当工具提示信息出现之后,我们将这个功能引入了我们自己的菜单,以便当用户将鼠标停留在某个菜单项上的时候,能够出现相应的提示信息。这一功能对于我们的用户来说非常重要,因为
用它可以解释为什么某个菜单项是被禁用的。由于我们的用户对 Windows 平台越来越熟悉,他们想要外观上更标准的菜单。现在我们使用了 CMenu,但是我们失去了
出色的菜单提示信息。请问如何在 MFC 中实现菜单提示信息呢?
Joakim Fagerli
多美妙的想法。Figure 1 的效果胜过千言万语。他展示了一个我写的菜单提示信息小程序——MenuTips,它实现了任何 MFC 应用程序均可复用的菜单提示信息。具备菜单提示信息特性真的很棒,因为它又排除了一个状态栏存在的理由。即便没有状态栏,你依然能够知晓每一个命令是做什么用的。更重要的是,提示信息
显示在每个菜单项旁边很更显眼。在当今的巨型显示器面前,很多用户甚至根本就意识不到出现在状态条上的菜单提示信息——它离人们的视线太远了。
Figure 1 菜单提示信息
我在类 CMenuTipManager 里面实现了菜单提示。如果你想在自己的应用程序中使用菜单提示功能,只需要在主窗口类中添加一个 CMenuTipManager对象,然后在
创建框架的时候调用 Install 即可:
//in CMainFrame::OnCreate(...)
m_menuTipManager.Install(this);
需要做的就这么多。现在当用户将鼠标停留在某个菜单项上面超过一秒钟,菜单提示信息管理器就会将对应的命令提示显示成一条提示信息,如 Figure 1 所示。CMenuTipManager 从你的程序的串表中获取提示信息,那也是 MFC
寻找状态栏提示信息的地方。
CMenuTipManager 使用了我闻名于世的子类化窗口类 CSubClassWnd 来捕获发往主窗口的 WM_MENUSELECT 消息。当用户在主菜单、系统菜单甚至上下文菜单中选中不同的的菜单项时,Windows 都会像
宿主窗口发送一个 WM_MENUSELECT 消息。如果你想提供反馈信息或者做其它自己你想做的事,此时便是最佳时机。MFC 的 CFrameWnd::OnMenuSelect 处理 WM_MENUSELECT 消息以便在状态栏上显示命令提示信息。CMenuTipManager 捕获同样的消息来显示菜单提示信息,
Figure 2 展示了相关的代码。
总体上来说,CMenuTipManager 还是非常容易理解的,但是在 Windows 中还是有几点需要注意。首先是工具提示信息本身:有人曾指出过如何使用
Windows 标准的工具提示信息么?我在 2000 年 9 月和 2001 年 6 月的专栏中使用的是 CPopupText 类。CPopupText 非常简单,甚至一个知道如何敲分号的 VB 专家都能够实现它。
你只需要实例化一个 CPopupText 对象,调用 Create 和 SetWindowText,然后 CPopupText::ShowDelayed 就会在指定的时间里显示提示信息了。CPopupText::Cancel 负责删除提示信息。唯一的难点是使 CPopupText 看起来和标准的工具提示信息一样。为了实现这一目的,CPopupText 使用了菜单字体并且调用 GetSystemColor(COLOR_INFOBK) 得到包含工具提示颜色的系统颜色。
具体细节请参考本文附带的源代码。
对于 CMenuTipManager 而言,最复杂的部分是如何放置提示信息,以便恰好与高亮菜单项的右面对齐。这个问题基本思路是先得到菜单的位置,然后进行一系列的算术运算将所有的菜单项高度加起来,直到达到了被选中的菜单项。但是怎样才能得到菜单的位置呢?这可不是一个简单的问题。你也许猜到了,菜单本身也是一个窗口,但是没有 API 可以用来得到它的句柄,那怎么办呢?我曾经多次提到,在
Windows 中总会有解决办法,你决不会被困住的。
CMenuTipManager 有一个静态的辅助函数 CMenuTipManager::GetRunningMenuWnd,它返回当前正在运行的菜单窗口。鉴于这个函数的使用频率非常高,我将其设定为公有。但这个函数是如何工作的呢?你也许考虑调用 WindowFromPoint 来得到位于鼠标下面的窗口。多数情况下这种方法能够达到目的,但是不要忽略一种情况:用户可能会通过键盘而非鼠标来调用菜单,此种情形下光标可能位于任何位置,而未必是在菜单上的。所以 CMenuTipManager 改为调用 ::EnumWindows 列举出所有顶层窗口,并且在其中寻找一个使用了特殊类名 #32768(Windows 为菜单窗口使用的类名)的窗口。
static BOOL MyEnumProc(HWND hwnd, LPARAM lParam)
{
char buf[16];
GetClassName(hwnd, buf, sizeof(buf));
if (strcmp(buf,"#32768")==0) { // menu window
// save hwnd
return FALSE; // no need to look further
}
return TRUE; // keep looking
}
因为只会显示一个菜单,所以 MyEnumProc 函数找到的第一个就恰恰是我们需要的。即便由于某些非常古怪的原因,有两个菜单同时出现,EnumWindows 也会按照z轴上自顶向下的顺序列举窗口,所以第一个被找到的菜单窗口也一定就是当前的活动菜单了。很聪明的做法不是么?一旦你找到了菜单窗口(HWND 或者 CWnd),剩下的就只是为提示信息的出现位置进行一些像素运算了。
Figure 2 中的 CMenuTipManager::OnMenuSelect 展示了细节工作。
那么提示信息文本怎么样呢?CMenuTipManager 提供了另外一个辅助函数, CMenuTipManager::GetMessageString,用以得到与每一个菜单命令相关联的提示信息字符串。这个函数是我或多或少地从 CFrameWnd::GetMessageString 直接拷贝过来的。为什么要复制这个函数?这样一来你就可以在没有主框架的情况下调用它了。CFrameWnd::GetMessageString 应该是静态的,但是不知道哪
位友好的微软员工在编写这个函数时显然没有注意到根本不需要 CFrameWnd。为什么在加载字符串资源的时候一定需要通过主框架窗口?为了通用性,我
创建自己的静态版本函数。
当我开始实现用户从菜单项上移开鼠标光标,提示信息必须消失的功能时,我遇到了另外一个非常奇怪的问题。对于主窗口而言,如果用户将鼠标指针移出菜单时,Windows 发送一个 WM_MENUSELECT 消息
,并且在消息中附带有父菜单句柄和一个 MF_POPUP 标志,这样一来就有可能知道所发生的事情从而隐藏提示信息。但是对于上下文菜单来说,就没有那么幸运了。当用户将鼠标移出上下文菜单时,并没有 WM_MENUSELECT 消息通知你。
没关系,在 Windows 中总会有解决方法。这种情况下,Windows 发送了一个不同的消息WM_ENTERIDLE。事实上,当程序等待输入并且对话框或者菜单被显示的时候,Windows 都会发送 WM_ENTERIDLE 消息。Windows 甚至通情达理到同时传递了对话框或者菜单的窗口句柄 HWND,吃惊吧?所以你所需要做的就是在接受到 WM_ENTERIDLE 消息的时候拿这个窗口句柄与鼠标下的窗口句柄进行比较。如果鼠标下的窗口句柄与随 WM_ENTERIDLE 发送过来的相同,那么鼠标仍然停留在菜单上面;如果鼠标下的是其
它窗口的句柄,那么说明用户已将鼠标移出上下文菜单,取消提示信息的时机到了。
Figure 2 中的 CMenuTipManager::OnEnterIdle 函数完成的就是这个功能。
最后,CMenuTipManager 使用了一个 m_bSticky 标记来控制提示信息是立即出现还是延迟一段时间之后才出现。当用户第一次使用某个菜单项的时候,菜单提示信息的出现是需要等待一段时间的。但是如果已经出现过一次提示信息,那么用户在选择其
它的新菜单项时就不必再等。所以一旦显示过提示信息,CMenuTipManager 就将 m_bSticky 置为 TRUE,以便随后的提示信息能够立即显示出来。取消菜单或者调用其
它命令将 m_bSticky 重新设置为 FALSE。
无论菜单项是处于启用还是禁用状态,CMenuTipManager 都会显示同样的提示信息。如果想在你的程序中显示为什么一个菜单项被禁用的信息,你就必须对 CMenuTipManager 和 MFC 的相关机制做一些改动。MFC 期待命令字串具备“长提示信息\n短提示信息”的格式,那意味着 MFC 总是预期有一个分隔长提示信息和短提示信息的换行符。MFC 将长提示信息显示在状态栏上、将短提示信息显示在工具栏上。你应该对这种处理方式进行扩展以便能够加入为什么菜单被禁
用的解释字符串。你必须将 CMenuTipManager::GetResCommandPrompt 函数改写为能够接受两个参数,以便适应命令提示信息为(long/short/disable)的格式,并且你需要改写 OnGetCommandPrompt 函数以便在菜单项有MF_DISABLED 标志时得到菜单项禁
用的提示信息。我将这部分工作留给读者作为练习。
祝大家编程愉快。
阅读(452) | 评论(0) | 转发(0) |