透明菜单






4.44/5 (20投票s)
2002 年 4 月 5 日
7分钟阅读

286545

4482
关于为您的应用程序创建透明菜单的文章
引言
Mac OS 拥有漂亮的透明菜单,它利用硬件加速。在 MS Windows 中要实现这种效果几乎是不可能的,但我们可以创建看起来像 Mac 菜单的菜单。本文的主题不是如何创建类似 Mac 的菜单,而是讨论 Windows 开发者可以用来创建漂亮的拥有者绘制菜单的一些选项。在本文中,我将介绍实现透明菜单的四种不同方法。
- 使用
WM_INITMENUPOPUP
消息 - 使用拥有者绘制方法
- 菜单类子类化
- 使用钩子
从 Win2K 开始的所有 Windows 版本都允许您设置窗口的透明度级别。由于 Windows 没有返回菜单窗口 HANDLE
的 API,上述每种方法都试图获取菜单的句柄并设置透明度。
为普通窗口设置透明度
Windows 2000 及更高版本提供了一个设置窗口透明度的函数
BOOL SetLayeredWindowAttributes(HWND hwnd,COLORREF crKey,BYTE bAlpha,DWORD dwFlags)
给定窗口的 HWND
,可以使用以下方式设置窗口的透明度。
//Setting transparency for a normal window
DWORD dwStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
//WS_EX_LAYERED Flag must be specified with extended styles
SetWindowLong(hWnd, GWL_EXSTYLE, dwStyle|WS_EX_LAYERED);
SetLayeredWindowAttributes(hWnd, 0, 150, LWA_ALPHA);
//150 represent transparency level
透明度级别可以从 0 到 255。零表示完全透明(不可见),255 表示完全不透明。
制作透明菜单
菜单的情况下会遇到一些复杂性。除了透明度功能,微软还提供了菜单动画,如果将上述代码应用于菜单窗口,这些动画会干扰透明度设置。因此,作为一种变通方法,我们将使用以下代码暂时禁用菜单动画
//Disable menu animation
OSVERSIONINFO ovi;
ovi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
BOOL bRet = GetVersionEx(&ovi);
// Toggle menu fading/animation mode (Windows2K or later)
BOOL bMenuFading = FALSE;
BOOL bMenuAnim = FALSE;
if( ovi.dwMajorVersion >= 5 ){
SystemParametersInfo(SPI_GETMENUFADE, 0, &bMenuFading, 0);
SystemParametersInfo(SPI_GETMENUANIMATION, 0, &bMenuAnim, 0);
SystemParametersInfo(SPI_SETMENUFADE, 0,
(LPVOID) FALSE, SPIF_SENDWININICHANGE);
SystemParametersInfo(SPI_SETMENUANIMATION, 0,
(LPVOID) FALSE, SPIF_SENDWININICHANGE);
}
//Transparency code goes here...
if( ovi.dwMajorVersion >= 5 ){
SystemParametersInfo(SPI_SETMENUFADE, 0,
(LPVOID) bMenuFading, SPIF_SENDWININICHANGE);
SystemParametersInfo(SPI_SETMENUANIMATION, 0,
(LPVOID) bMenuAnim, SPIF_SENDWININICHANGE);
}
为了简洁起见,我将在下文中省略上述代码片段。但它对于正常工作是必需的。
1. 处理 WM_INITMENUPOPUP
在此方法中,我们将处理 WM_INITMENUPOPUP
消息。根据 MSDN,“当下拉菜单或子菜单即将激活时,将发送 WM_INITMENUPOPUP
消息。这允许应用程序在显示菜单之前修改菜单,而无需更改整个菜单”。要做的是,当生成 WM_INITMENUPOPUP
时,菜单已经创建。因此,我们将使用 FindWindow()
API 来获取菜单的 HWND
。然后,我们将修改菜单以满足我们的目的。菜单的类名是 #32768
。代码如下
//Main window message handler procedure
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITMENUPOPUP:
{
HWND hMenuWnd = FindWindow(_T("#32768"), NULL);
if (IsWindow(hMenuWnd)) {
DWORD dwStyle;
dwStyle = GetWindowLong(hMenuWnd, GWL_EXSTYLE);
dwStyle = SetWindowLong(hMenuWnd, GWL_EXSTYLE,
dwStyle|WS_EX_LAYERED);
SetLayeredWindowAttributes(hMenuWnd, 0, 150, LWA_ALPHA);
}
}
break;
case WM_CONTEXTMENU:
{
HMENU hMenu = LoadMenu(hInst, MAKEINTRESOURCE(IDC_TMENU1));
if (IsMenu(hMenu)) {
DWORD dwPos = ::GetMessagePos();
POINT pt = {LOWORD(dwPos), HIWORD(dwPos)};
HMENU hSubMenu = GetSubMenu(hMenu, 0);
TrackPopupMenuEx(hSubMenu, TPM_LEFTALIGN |
TPM_RIGHTBUTTON | TPM_NOANIMATION,
pt.x, pt.y, hWnd, NULL);
}
}
break;
//Other handlers go here...
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
请注意,此技术仅适用于顶级窗口,因此不幸的是,您无法将其应用于单个控件。在我测试期间,我收到了所有菜单的 WM_INITMENUPOPUP
,但 SetWindowLong()
对除上下文菜单以外的所有菜单都失败了,即使是上下文菜单中的弹出菜单也失败了。下面是一个屏幕截图
因此,它的应用仅限于类似上下文菜单的用途。另一个值得注意的点是,可以不使用 SystemParametersInfo()
来关闭菜单动画。这是因为 TrackPopupMenu()
/ TrackPopupMenuEx()
可以接受一个 TPM_NOANIMATION
参数,该参数可以阻止菜单动画。您可以有效地在只需要简单上下文菜单和按钮菜单的应用程序中使用此方法。随附的项目 tMenu1
说明了此方法。
2. 拥有者绘制菜单
如果您有拥有者绘制菜单,那么这个方法适合您。当我们收到 WM_DRAWITEM
消息时,我们还将收到菜单的 HDC。因此,我们可以使用 WindowFromDC()
API 获取菜单窗口的句柄。由于不稳定的菜单动画,此调用并不总是返回有效的句柄。Windows 为菜单动画提供了一个临时 DC。但是一旦我们获得了一个有效的 DC,我们就可以对该菜单窗口执行所有我们需要的操作。因此,您需要为每个菜单项设置 MF_OWNERDRAW
样式。
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_MEASUREITEM:
{
LPMEASUREITEMSTRUCT lpms = (LPMEASUREITEMSTRUCT)lParam;
lpms->itemWidth = 80;
lpms->itemHeight=40;
}
return 0;
case WM_DRAWITEM:
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) lParam;
HWND hMenuWnd = WindowFromDC(lpdis->hDC);
if (IsWindow(hMenuWnd)) {
DWORD dwStyle =0;
dwStyle = GetWindowLong(hMenuWnd, GWL_EXSTYLE);
SetWindowLong(hMenuWnd, GWL_EXSTYLE, dwStyle|WS_EX_LAYERED);
SetLayeredWindowAttributes(hMenuWnd, 0, 150, LWA_ALPHA);
}
//TODO:Rest of the drawing stuff...
}
return 0;
case WM_CONTEXTMENU:
{
HMENU hMenu = CreateMenu();
HMENU hMenuPopup = CreatePopupMenu();
AppendMenu(hMenuPopup, MF_STRING|MF_OWNERDRAW,
ID_FILE_NEW, (LPCTSTR)"&Pub");
AppendMenu(hMenuPopup, MF_STRING|MF_OWNERDRAW,
ID_FILE_OPEN, (LPCTSTR)"&Club");
AppendMenu(hMenu, MF_POPUP|MF_OWNERDRAW,
(UINT)hMenuPopup, (LPCTSTR)"&labamba0");
DWORD dwPos = ::GetMessagePos();
POINT pt = {LOWORD(dwPos), HIWORD(dwPos)};
TrackPopupMenuEx(hMenuPopup, TPM_LEFTALIGN | TPM_RIGHTBUTTON
|TPM_NOANIMATION, pt.x, pt.y, hWnd, NULL);
DestroyMenu(hMenu);
}
return 0;
//Rest of message handler
}//switch
}
这里也会出现一些小问题,当然是由菜单动画引起的。您必须将鼠标指针悬停在菜单上才能使其透明。为了克服这种不良效果,您应该在 TrackPopupMenuEx()
标志中指定 TPM_NOANIMATION
。但它在弹出菜单方面仍然存在一些问题。这是屏幕截图
这个方法可以非常容易地集成到拥有者绘制菜单中。它可以在不经过太多繁琐工作的情况下提供漂亮的透明效果。随附的项目 tMenu0
说明了此方法。
3. 菜单子类化
这是为数不多的方法之一。我从未见过菜单类被子类化的实例。在此方法中,我们使用 GetClassInfo()
/ GetClassInfoEx()
获取有关菜单类的信息,并用我们的窗口过程替换它的窗口过程,然后用相同的名称重新注册菜单类。因此,之后创建的所有菜单都将使用我们的窗口过程。菜单的类名是 #32768
。以下代码说明了如何实现。
//Superclassing menu
LRESULT CALLBACK MenuWndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,int nCmdShow)
{
WNDCLASS wc;
GetClassInfo(NULL, _T("#32768"), &wc);
wc.hInstance = hInstance;
wpOldMenuProc = wc.lpfnWndProc;
wc.lpfnWndProc = (WNDPROC)MenuWndProc;
SetLastError(0);
RegisterClass(&wc);
//Rest of the stuff goes here...
return 0;
}
现在 MenuWndProc
可以这样写
//Message Handler for Menu
LRESULT CALLBACK MenuWndProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
case WM_CREATE:
{
LRESULT lRet = CallWindowProc(wpOldMenuProc,
hWnd, uMsg, wParam, lParam);
DWORD dwStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
dwStyle &= ~WS_EX_WINDOWEDGE;
dwStyle &= ~WS_EX_DLGMODALFRAME;
SetWindowLong(hWnd, GWL_EXSTYLE, dwStyle|WS_EX_LAYERED);
SetLayeredWindowAttributes(hWnd, 0, 180, LWA_ALPHA);
return lRet;
}
default:
return CallWindowProc(wpOldMenuProc, hWnd, uMsg, wParam, lParam);
}//switch
return 0;
}
此方法对所有类型的菜单都非常有效。它不限于上下文菜单。它还可以处理弹出菜单。但一如既往,“淡入淡出效果”动画样式可能会引起一些问题。它有时会干扰菜单。随附的示例项目 tMenu2
说明了此方法。
4. 使用 Windows 钩子
钩子代表了 Windows 的强大功能之一。几乎 Windows 中的一切都可以使用钩子进行监视。正如 MSDN 所说,它们功能强大,应谨慎使用,否则可能会弄乱一切。通过“挂钩”,您将一个函数(也称为过滤器函数或钩子过程)告知 Windows,当您感兴趣的事件发生时,就会调用该函数。它们有两种类型:本地和远程钩子。我们对本地钩子感兴趣,因为它们会捕获本地线程中发生的事件。
当您创建钩子时,Windows 会在内存中创建一个包含钩子信息的结构,并将其添加到现有钩子的链表中。新钩子添加到旧钩子前面。当事件发生时,如果您安装了本地钩子,则会在您的进程中调用过滤器函数,因此相当直接。可以使用 SetWindowsHookEx()
函数安装钩子。可以通过调用 UnhookWindowsHookEx()
来卸载钩子,该函数只接受一个参数,即您想要卸载的钩子的句柄。当事件发生时,Windows 会调用 HookProcedure
。
我们将使用 WH_CALLWNDPROC
,当调用 SendMessage()
时会启动它。然后,我们将通过检查类名来检查窗口是否为菜单窗口。如果是,我们将继续进行透明度操作。以下代码说明了这一点
//Main window Procedure
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
static HWND hWndEdit;
static HHOOK hHook;
switch (message)
{
case WM_CREATE:
hHook = SetWindowsHookEx(WH_CALLWNDPROC, HookCallWndProc,
0,GetWindowThreadProcessId(hWnd,0));
hWndEdit = CreateWindowEx(WS_EX_CLIENTEDGE, _T("EDIT"),NULL,
WS_CHILD | WS_VISIBLE | ES_MULTILINE | WS_HSCROLL | WS_VSCROLL,
0,0,0,0,hWnd,(HMENU)NULL,hInst,NULL;
SendMessage(hWndEdit, WM_SETFONT, (
WPARAM)GetStockObject(SYSTEM_FIXED_FONT), MAKELPARAM(TRUE, 0));
break;
case WM_DESTROY:
if(hHook!=0)
UnhookWindowsHookEx(hHook);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
LRESULT CALLBACK HookCallWndProc(int nCode,
WPARAM wParam, LPARAM lParam)
{
CWPSTRUCT cwps;
LONG lRet;
if( nCode == HC_ACTION ){
CopyMemory(&cwps, (LPVOID)lParam, sizeof(CWPSTRUCT));
switch(cwps.message){
case WM_CREATE:
{
CHAR szClass[128];
GetClassName(cwps.hwnd, szClass, 127);
if(_tcscmp(szClass, _T("#32768"))==0){
HWND hMenuWnd = cwps.hwnd;
DWORD dwStyle =0;
dwStyle = GetWindowLong(hMenuWnd, GWL_EXSTYLE);
SetWindowLong(hMenuWnd, GWL_EXSTYLE, dwStyle|WS_EX_LAYERED);
SetLayeredWindowAttributes(hMenuWnd, 0, 220, LWA_ALPHA);
}
}
break;
}//switch
}//if
return CallNextHookEx((HHOOK)WH_CALLWNDPROC, nCode, wParam, lParam);
}
钩子通过链表链接,最近安装的钩子位于链表的头部。当事件发生时,Windows 只会调用链中的第一个钩子。您的钩子过程有责任调用链中的下一个钩子。您可以选择不调用下一个钩子,但最好知道您在做什么。大多数时候,调用下一个过程是一个好习惯,这样其他钩子都可以有机会处理该事件。您可以通过调用 CallNextHookEx()
来调用下一个钩子
如您所见,这是一项稍微复杂的技术。但它非常有用,因为可以在 HookWindow
过程中捕获任何窗口。有关 Windows 钩子的更多信息,您可以访问 Iczelion 的 Win32 Assembly Homepage,尽管它是汇编语言,但非常有益。此方法在随附的 tMenu3
示例项目中有所说明。第一个屏幕截图适用于最后两种技术。
选择哪一个...
这个问题的答案完全取决于您的应用程序。如果您的应用程序很简单,例如在对话框中,那么您可以选择前两种方法中的任何一种。您可以选择最后两种方法中的任何一种来获得对应用程序中每个菜单项的支持。如果您使用钩子,您应该非常小心,并且在不需要时必须取消挂钩。通过使用适当的透明度级别,您可以使您的应用程序拥有外观漂亮的透明菜单。这一点很重要,因为过度的透明度可能会使用户看不到菜单文本。我推荐的值在 220 到 245 之间。
结语...
本文初次发布时,只包含了钩子方法。但读者提出了许多其他方法。我已纳入所有这些建议,并为它们提供了示例。我认为本文有一些有价值的内容。感谢大家提出的宝贵建议,并欢迎继续提出。
许可证
本文没有明确的许可证附加到它,但可能包含在文章文本或下载文件本身中的使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。