带客户端阴影的动画对话框菜单





3.00/5 (12投票s)
2003年10月24日
6分钟阅读

77714

1223
本文介绍了如何在任何 CWnd 上创建动画菜单。
引言
你见过没有菜单项,或者只有一个弹出项的菜单吗?例如,VC 标准创建的帮助菜单只有“关于”一项。一些应用程序以非常基本的菜单面板开始。为什么不显示单个菜单项背后的对话框,而不是菜单项本身。这就是我带来的新想法!
工作原理
在 Windows 98、NT5 及更高版本上制作动画对话框菜单非常简单,而解决旧系统上的问题则复杂得多。这是我的代码的第一个版本,肯定有很多“bug”。我希望它对某人有用。
关键在于 Windows API 中深藏的 AnimateWindow
函数。在使用“初步”Windows API 函数时,一个好主意是在任何头文件包含到应用程序代码之前添加这两行:
#undef WINVER #define WINVER 1000000
嗯,这并不是一个真正的好主意,但对于 Visual C++ 6 配合某些服务包和 Windows XP Pro SP1 来说,这是唯一的选择(而且,它确实有效!)。你应该首先检查 AnimateWindow
的调用是否在没有这些宏的情况下工作。WINVER 是 VC 定义的一个宏,用于识别你正在为其构建的 Windows 版本。它被预定义,API 声明在需要某些兼容性信息时会引用它。编译时,你会收到关于如何进行稳定构建的通知。
我创建了一个基于对话框的应用程序,它本身包含一个充当菜单栏的子对话框(名为 CDummy
)。菜单栏对话框通过弹出对话框来处理用户请求。它还负责绘制效果以及大部分的清理/重绘操作。简单的按钮控制弹出操作。我使用的一个小技巧是,按钮的 ID 是连续的 1026、1027、1028,这样我就可以根据需要通过偏移量来访问它们。这在演示项目中并未实现。
绘制菜单的代码非常简单(而且速度慢),最好的解释就是直接阅读代码。
void CDummy::ShowDialogMenu(int number, int initial_control) { HideAllDialogMenus(); if(!statMenu[number]) { CRect rc; GetDlgItem(initial_control)->GetClientRect(&rc); GetDlgItem(initial_control)->ClientToScreen(&rc); this->ScreenToClient(&rc); listMenu[number]->SetWindowPos(0,rc.left,rc.bottom, 0,0,SWP_NOSIZE|SWP_HIDEWINDOW); AnimateWindow(listMenu[number]->GetSafeHwnd(),300, AW_ACTIVATE|AW_SLIDE|AW_VER_POSITIVE|AW_HOR_POSITIVE); CWindowDC dc(0); CDC tdc; statMenu[number]=1; listMenu[number]->GetClientRect(&rc); listMenu[number]->ClientToScreen(&rc); dc.MoveTo(rc.left,rc.top); dc.LineTo(rc.right,rc.top); dc.LineTo(rc.right,rc.bottom); dc.LineTo(rc.left,rc.bottom); dc.LineTo(rc.left,rc.top); tdc.CreateCompatibleDC(&dc); CBitmap map1; map1.CreateCompatibleBitmap(&dc,1600,1200); tdc.SelectObject(&map1); tdc.BitBlt(rc.left,rc.top,rc.right+S_SHADE, rc.bottom+S_SHADE,&dc,rc.left,rc.top,SRCCOPY); for(int w=rc.right;w<=rc.right+S_SHADE;w++) { float f=((float)w-rc.right)/S_SHADE; for(int e=rc.top+S_SHADE;e< rc.bottom;e++) { tdc.SetPixel(w,e,RGB( GetRValue(tdc.GetPixel(w,e))/(2-f), GetGValue(tdc.GetPixel(w,e))/(2-f), GetBValue(tdc.GetPixel(w,e))/(2-f))); } } for(int e=rc.bottom;e<=rc.bottom+S_SHADE;e++) { float f=((float)e-rc.bottom)/S_SHADE; for(w=rc.left+S_SHADE;w<=rc.right+1;w++) { tdc.SetPixel(w,e,RGB( GetRValue(tdc.GetPixel(w,e))/(2-f), GetGValue(tdc.GetPixel(w,e))/(2-f), GetBValue(tdc.GetPixel(w,e))/(2-f))); } } for(e=rc.bottom;e<=rc.bottom+S_SHADE-2;e++) { float f=((float)e-rc.bottom)/S_SHADE; for(w=rc.right+2;w<=rc.right+S_SHADE-2;w++) { float g=((float)w-rc.right+2)/(S_SHADE); tdc.SetPixel(w,e,RGB( GetRValue(tdc.GetPixel(w,e))/(2-(f+g)/2), GetGValue(tdc.GetPixel(w,e))/(2-(f+g)/2), GetBValue(tdc.GetPixel(w,e))/(2-(f+g)/2))); } } dc.BitBlt(rc.left,rc.top,rc.right+S_SHADE,rc.bottom+S_SHADE, &tdc,rc.left,rc.top,SRCCOPY); } }
阴影完全是所有者绘制的,它直接绘制在桌面窗口上。这会导致一些清理重绘问题,但对于阴影来说没问题。我很快就会更新为弹出窗口,那时会更有意义。然而,如果你关心效率或绘图安全,你可以简单地使用 CClientDC
进行绘制。隐藏菜单例程只是一个重绘。
值得一提的是,你可以通过修改 AnimateWindow
中的标志来更改动画类型。例如,将 AW_ACTIVATE | AW_SLIDE | AW_VER_POSITIVE | AW_HOR_POSITIVE
更改为 AW_ACTIVATE | AW_SLIDE | AW_HOR_POSITIVE
或 AW_ACTIVATE | AW_SLIDE | AW_VER_POSITIVE
或 MSDN 中 AnimateWindow
定义的其他内容。
在你的应用程序中使用代码
我没有尝试创建一个类来实现这个功能,因为我想保持用户控制对话框的自定义方式。我很乐意在此接受建议。总之,我设法将以下步骤分开,这应该能让你在应用程序中更轻松地使用代码:
- 创建你想要显示为菜单的所有对话框。建议使用
CDialog
继承的对话框。在属性中选择“child”(子对话框),勾选“control”(控件)和“visible”(可见),并设置“foreground”(前台)。如果你不指定任何这些样式,菜单可能无法正确绘制。它们都与 Z 顺序和父窗口管理有关,我不确定具体原因。你知道哪里可以找到细节,对吧。哦,别忘了在你的CDummy
源文件中包含正确的头文件,以便创建实例。
- 创建一个菜单栏对话框,比如
CDummy
。这个对话框继承自CDialog
,但它也可以是任何基于CWnd
的东西。对于 SDK 爱好者来说,你可以使用直接句柄,并进行一些修改来消除 MFC 设备上下文。在你的主窗口类中,声明一个CDummy
的实例,并在OnInitDialog()
(例如)中像这样初始化它:dummy=new CDummy(this); dummy->Create(IDD_DIALOG2,this); dummy->SetWindowPos(0,0,0,0,0,SWP_NOSIZE|SWP_SHOWWINDOW);
- 声明数组来存储你的对话框指针和当前状态(在演示项目中:listMenu 和 statMenu)。创建你的菜单对话框实例,并在
WM_INITDIALOG
处理程序中放入初始化代码,像这样:BOOL CDummy::OnInitDialog() { CDialog::OnInitDialog(); memset(listMenu,0,sizeof(listMenu)); menu1.Create(IDD_DIALOG1,this->GetParent()); menu1.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW); menu2.Create(IDD_DIALOG3,this->GetParent()); menu2.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW); menu3.Create(IDD_DIALOG4,this->GetParent()); menu3.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW); menu4.Create(IDD_DIALOG5,this->GetParent()); menu4.SetWindowPos(0,-1000,-1000,0,0,SWP_NOSIZE|SWP_SHOWWINDOW); listMenu[0]=&menu1; listMenu[1]=&menu2; listMenu[2]=&menu3; listMenu[3]=&menu4; return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE }
- 触发菜单显示或隐藏的事件必须发生在
CDummy
中。放入一些按钮或其他控件并处理事件。在演示项目中,我使用了简单的按钮。这是我处理其中一个按钮的BN_CLICKED
消息的方式:void CDummy::OnButton1() { ShowDialogMenu(0,IDC_BUTTON1); }
ShowDialogMenu(int arg1,int arg2)
会在屏幕上显示对话框。arg1
表示应该显示哪个对话框,例如,0 表示显示listMenu[0]
引用的对话框。arg2
是对话框将要绘制的控件(静态文本、按钮、编辑框、复选框,等等)旁边的 ID。该函数获取控件的客户端矩形,并在矩形下方绘制对话框。
- 在主窗口中处理
WM_KILLFOCUS
,这是所有菜单对话框都必须是子对话框的原因之一。void CDmenuDlg::OnKillFocus(CWnd* pNewWnd) { CDialog::OnKillFocus(pNewWnd); dummy->HideAllDialogMenus(); }
- 在主窗口中处理
WM_ACTIVATE
,这种方式可以修复“失去焦点后的重绘”问题,但并非总是有效(见下文)。void CDmenuDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { CDialog::OnActivate(nState, pWndOther, bMinimized); dummy->HideAllDialogMenus(); Invalidate(); }
已知问题
Visual C++ 或我的代码中有几个 bug。
- 有时
AnimateWindow
不起作用,但返回ERROR_SUCCESS
。演示项目应用程序永远不会发生这种情况,但你的应用程序可能没那么幸运。我还没有找出导致问题的原因,但它适用于所有初步函数。 - 点击主窗口上的控件不会使菜单消失,而是会覆盖在菜单上绘制控件。要修复这个问题,你可以使用消息反射或手动将消息转发到主窗口,但这需要对主窗口上的所有控件都这样做。我正在研究另一个解决方案。
- 如果一个菜单处于活动状态,而应用程序失去了焦点,将会发生不正确的重绘。最后一个问题对我来说是个谜!
- 在 Windows 98 和 ME 上,并行任务配合得不是很好,我的意思是它们由于糟糕的 OS 线程而运行缓慢。不要让应用程序同时处理太多活动线程。
五角大楼雇佣我来做这个项目。他们需要一些软件来附加到控制界面。我没有包含你看到的驱动程序,但我可能不小心删除了/遗漏了一些其他代码。保持警惕!
嗯,开玩笑的,但保持警惕,我可能忘记提到一些让你成功的小技巧。这是一个添加“如果这段代码有效,那就是 Vladimir Ralev 写的,否则我不知道是谁写的!”的好地方,就像 MSDN 的作者一样。
感谢所有评论和修复。
致谢
作者:Vladimir Ralev <v_ralev@yahoo.com>,2003 年 10 月 20 日
特别感谢 PJ Naughter 在 CTreeFileCtrl
& CSortedArray
v1.06 上的杰出工作。我使用了(稍微修改过的)CTreeFileCtrl
来演示如何在无需弹出模态对话框的情况下轻松选择文件。