用HOOK机制让自绘菜单栏控件模拟系统菜单栏行为

来源:互联网 发布:linux远程重启服务器 编辑:程序博客网 时间:2024/05/16 11:22

在Windows的UI开发中,我们经常需要对窗口和控件进行自绘,以实现独特的UI风格,满足软件的个性化需求,提升用户的视觉体验。目前市面上流行的软件,大都有自己别具一格的界面,许多软件还提供多种界面供用户选择,我们称之为skin。应用程序的类型,按M$通俗粗略的划分,可以分为:对话框程序、单文档程序(SDI)、多文档程序(MDI)。在换肤功能的开发中,对话框程序是最简单的,单文档和多文档程序是比较头痛的。因为,我们会遇到诸如菜单栏、工具栏的换肤问题(对话框也可能有菜单栏、工具栏,只要有这些,就是比较麻烦的事情。)。 这里,我们先讨论一下关于菜单栏换肤时遇到的一个问题。

我们通常用自制的菜单栏控件替代系统的菜单栏来解决换肤的问题。大致的方案是,新建一个窗口用作菜单栏控件,在该窗口上绘制出菜单栏的外观,处理WM_PAINT、WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_LBUTTONUP和WM_MOUSELEAVE消息,鼠标移动时命中顶层菜单并使之高亮,鼠标按下时使用TrackPopupMenuEx函数弹出被命中顶层菜单的子菜单。

由于是自绘控件,换肤变得极其容易,想怎么绘制就怎么绘制,为换肤提供极大的灵活性。可是,现在却能遇到了一个难题。TrackPopupMenuEx函数弹出子菜单后不会立即返回,相当于弹出的子菜单是一个模态窗口。而事实上,子菜单的确是一个窗口。通过SetWindowsHookEx设置WH_CALLWNDPROC方式的hook可以hook到菜单是一个类名为”#32768″的窗口。菜单的这种特性导致了我们的自绘控件与系统的菜单栏行为不一致!在系统的菜单栏上,我们在一个顶层菜单上点击鼠标弹出其子菜单后,系统菜单栏仍能响应鼠标消息,可以把鼠标移动到别的顶层菜单上,这时当前的弹出菜单自动关闭,下一个顶层菜单的子菜单自动弹出。而我们的自绘控件却没有这种特性,子菜单一旦弹出,直到关闭,自绘菜单栏无法响应菜单的消息,移动到其它顶层菜单上不会关闭当前的菜单,也不会弹出下一个子菜单。

这个问题的根源在于,消息循环在弹出子菜单后被TrackPopupMenuEx阻塞无法获得新消息,子菜单以模态方式展示给用户,直到用户选择或取消。为了模拟出系统菜单栏的行为,我们设想TrackPopupMenuEx函数返回前,自绘控件仍可以处理鼠标消息,当鼠标移动到新的顶层菜单上时关闭当前的子菜单并弹出新的子菜单。

为了获得这种能力,我们不得不使用强大的hook机制。在TrackPopupMenuEx函数调用前用SetWindowsHookEx函数设置WH_MSGFILTER类型的hook,让TrackPopupMenuEx返回前也可以获得鼠标消息。在TrackPopupMenuEx返回后,我们调用UnhookWindowsHookEx函数卸除hook。在消息HOOK中,我们判断code是否为MSGF_MENU,若不是则用CallNextHookEx函数调用下一HOOK,若是MSGF_MENU,我们检查消息类型是否为WM_MOUSEMOVE。在WM_MOUSEMOVE消息时,我们调用HitTestMenuItem函数来测试哪一个顶层菜单被命中,如果命中的菜单发生变化,则通过发送WM_CANCELMODE消息关闭当前的弹出菜单。一旦弹出菜单被关闭,则TrackPopupMenuEx函数立即返回,TrackPopupMenuEx函数后面的代码得以继续执行。

基本的原理解释差不多了,我们直接看看关键的示例性代码吧!

我定义了一个叫做CZJMenuBar的类,在类中声明了几个静态成员:
static HHOOK s_hMsgHook;
static HWND s_hWnd;
static int s_nNextMenuItem;
static int s_nCurrMenuItem;
static CZJMenuBar* s_pMenuBar;

弹出菜单时,调用PopupMenu函数:

void CZJMenuBar::PopupMenu()
{
        CWindowDC dc(this);
        while (s_nNextMenuItem > -1)
        {
                if (s_nNextMenuItem == -1 || s_nNextMenuItem >= (int)m_vecMenuItems.size())
                {
                        return;
               }

                LPZJMenuItemData pMenuItem = m_vecMenuItems[s_nNextMenuItem];

                s_pMenuBar = this;
                s_nCurrMenuItem = s_nNextMenuItem;
                s_hWnd = m_hWnd;
                s_hMsgHook = SetWindowsHookEx(WH_MSGFILTER, MessageProc, 0, GetCurrentThreadId());

                m_bMenuPressed = TRUE;
                InvalidateRect(pMenuItem->m_rtMenu, TRUE);

                CRect rect;
                GetWindowRect(rect);
                CPoint ptMenu(pMenuItem->m_rtText.left + rect.left, pMenuItem->m_rtText.bottom + rect.top);
                pMenuItem->m_yMenuTop = ptMenu.y - 1;
                TrackPopupMenuEx(pMenuItem->m_hMenu, 0, ptMenu.x, ptMenu.y - 1, m_hWnd, NULL);

                if (s_hMsgHook != NULL) UnhookWindowsHookEx(s_hMsgHook);
                if (s_nNextMenuItem == s_nCurrMenuItem)
                {
                        m_bMenuPressed = FALSE;
                        InvalidateRect(pMenuItem->m_rtMenu, TRUE);
                        break;
                }
                else
                {
                        SetSelectMenuItem(s_nNextMenuItem);
                }
                s_nCurrMenuItem = -1;
                m_bMenuPressed = FALSE;
        }

        s_nCurrMenuItem = -1;
        s_nNextMenuItem = -1;
        m_bMenuPressed = FALSE;
} 

下面是HOOK的代码,在HOOK函数中,只关心WM_MOUSEMOVE消息,对该消息进行处理,测试新的菜单命中项,必要时关闭当前的弹出菜单:
LRESULT CALLBACK CZJMenuBar::MessageProc(int code, WPARAM wParam, LPARAM lParam)
{
        if (code == MSGF_MENU)
        {
                HookMessageProc((MSG*)lParam);
        }
        return ::CallNextHookEx(s_hMsgHook, code, wParam, lParam);
}
void CZJMenuBar::HookMessageProc(MSG* pMsg)
{
        if (pMsg->message == WM_MOUSEMOVE)
        {
                CPoint point = pMsg->pt;
                CRect rect;
                s_pMenuBar->GetWindowRect(&rect);
                point.x = point.x - rect.left;
                point.y = point.y - rect.top;
                s_nNextMenuItem = s_pMenuBar->HitTestMenuItem(point);
                if (s_nNextMenuItem > -1 && s_nNextMenuItem != s_nCurrMenuItem)
                {
                        s_pMenuBar->SendMessage(WM_CANCELMODE, 0, 0);
                }
        }
}

经过这样的处理,这个自绘菜单栏就与系统菜单栏的行为一致了,可以完全取代系统的菜单栏,配合窗体的换肤技术,方便地实现软件界面的换肤。

另外有一个问题,当我们在有子菜单弹出的情况下,在菜单栏上快速地移动鼠标时,顶层菜单栏的刷新有些问题,并不像系统的菜单栏那样每个被鼠标命中的顶层菜单都有push状态,而是鼠标最近停留的顶层菜单才显示为push状态,中间连续切换的顶层菜单的push状态被跳过了。这个问题出现的原因与我在《在消息响应函数中立即处理PENDING WM_PAINT解决刷新问题》一文中分析的完全一样,参照那篇文章提到的方法可以完全解决。

原创粉丝点击