Single Class DevStudio-Like Menu Bar

.
Home site www.spunge.com/~alagesan

The latest code will be available at www.spunge.org/~codebee.



Click here for a larger image.

Environment: VC5, Windows NT4 SP3, Windows 9x and above

Single Class Plug & Play DevStudio-Like MFC Menu Bar

CMenuBar is simple Menubar Control that can be integrated into any MDI MFC application by making a few changes in CMainFrame. The simplicity of the CMenuBar is that it uses the Windows Menu system to show a popup menu. It only overrides the CalcDynamicLayout() function to make the Menu Bar resizable docking bar. It simulates all the behaviour of a Windows menu system and menu messages such as WM_MENUCHAR, WM_INITMENU, WM_INITPOPUPMENU, WM_MENUSELECT, and so forth. The MenuBar is UniCode ready.

The CMenuBar class is derived from the CControlBar class. To make it dock and float like a DevStudio Menu Bar, the functions of CControlBar are overridden.

Function Description
Create() Initializes Menu Bar
CalcDynamicLayout() Calculates Menu Bar layout
OnUpdateCmdUI() Checks change of Menu Bar position and size, and to disable the Close button when floating

The CMenuBar subclasses the CMainFrame and MDIClient windows in the SubClassMDI() function, where it also removes the main and refresh menus of CMainFrame to shift it to the MenuBar control.

VOID CMenuBar::SubClassMDI( CWnd* pMainFrame )
{
    g_pMenuBar = this;
    m_pMainFrame = (CMDIFrameWnd*)pMainFrame;

    /// Type cast to retrieve the protected members
    /// m_hWndMDIClient, m_hMenuDefault
    _CMBMDIFrameWnd* _pMainFrame = (_CMBMDIFrameWnd*)pMainFrame;
    HWND hwndClient = _pMainFrame->m_hWndMDIClient;
    HMENU hmnuMDIMainDef = _pMainFrame->m_hMenuDefault;

    /// Subclass MainFrame and MDIClient Windows
    _gs_pfnMDIFrameProc = (WNDPROC)::SetWindowLong
                                     ( m_pMainFrame->m_hWnd,
                                       GWL_WNDPROC,
                                       (DWORD)_MDIFrameProc );
    _gs_pfnMDIClientProc = (WNDPROC)::SetWindowLong
                                      ( hwndClient, GWL_WNDPROC,
                                        (DWORD)_MDIClientProc );

    /// Get Window Popup of main menu
    HMENU hWindowMenu = NULL;
    INT nCnt          = GetMenuItemCount( hmnuMDIMainDef );
    if( nCnt > 2 )
        hWindowMenu = GetSubMenu( hmnuMDIMainDef, nCnt - 2 );

    /// Now activate Menu Bar
    ::SendMessage( hwndClient, WM_MDISETMENU,
                   (WPARAM)hmnuMDIMainDef, (LPARAM)hWindowMenu );

    /// Hide MainFrame main menu
    ::SetMenu( m_pMainFrame->m_hWnd, NULL );

    _gs_hhAppKbHookProc = SetWindowsHookEx
                             ( WH_KEYBOARD, _AppKbHookProc, NULL,
                               ::GetCurrentThreadId() );
}

Each time a menu is being updated by CMainFrame or a MDIChild, the WM_MDISETMENU message (see function _MDIClientProc()) will come to MDIClient, which is trapped and sends a NULL menu handle to DefaultProc() so that the menu will not be visible in MainFrame. Take care to update the menu using only a WM_MDISETMENU message to avoid the unexpected exposure of the Main menu in MainFrame.

The MenuBar uses the menu handle, gotten in the message, to draw menu items. The menu item string and position are stored in m_arpItem, which was calculated by the RecalcItemPos() function.

The menu handle is passed to the CMenuBar::OnSetMDIMenu() function. The OnSetMDIMenu() function should only be called by using PostMessage(), because at the time of the WM_MDISETMENU message, the MDIChild was not activated and will fail to update the Icon and System buttons if maximized.

In the OnSetMDIMenu() function, to calculate menu item(s)' positions, RecalcItemPos() is called. RecalcItemPos() is also called from OnUpdateCmdUI() on the changes of the MDIChild maximize state and MenuBar position.

void CMenuBar::OnUpdateCmdUI( CFrameWnd* pTarget,
                              BOOL bDisableIfNoHndler )
{
    if( NULL == m_pMainFrame )
        return;

    if( _Is_Floating() != IsFloating() )
    {
        if( IsFloating() )
        {
            _Set_Floating();

            /// If menu bar is floating, disble; close button
            /// if enabled
            if( m_pDockBar )
            {
                HMENU hMenu = ::GetSystemMenu( m_pDockBar->
                                               GetDockingFrame()->
                                               m_hWnd, FALSE );
                if( hMenu )
                {
                    INT nState = GetMenuState( hMenu, SC_CLOSE,
                                               MF_BYCOMMAND );
                    if( 0 == (nState & (MF_DISABLED | MF_GRAYED)) )
                        EnableMenuItem( hMenu, SC_CLOSE,
                                        MF_BYCOMMAND |
                                        MF_DISABLED | MF_GRAYED );
                }
            }
        }
        else
            _Reset_Floating();
    }

    if( _Is_LayoutUpdated() )
    {
        /// If Client rectangle differs that was stored,
        /// refresh menu
        CRect rect;
        GetClientRect( rect );

        _Reset_LayoutUpdated();
        InvalidateRect( NULL, FALSE );
        if( rect.Width() != m_cbBarLastSize.cx || rect.Height()
                         != m_cbBarLastSize.cy )
        {
            ResetContent();
            RecalcItemPos();
        }
    }
}

Now, the RecalcItemPos() function is able to find the Menu item position for Vertical docking and the Menu breaks at the item that goes off the Menu Bar's visibility. Those items removed at the Menu break will be shown as popups.

In the vertical docking state, Menu items are first rendered horizontally and their bitmaps rotated to 90 degrees by using the _RotateBmp90() function. See the functions used by _RotateBmp90(). It will interesting, useful, and you'll learn some image processing techinques.

The MainFrame is hooked by the _MDIFrameProc() function to get the WM_ACTIVATEAPP message to deactivate the menu if it is active on the message.

The active ChildFrames is also hooked by _MDIChildProc() to get the WM_WINDOWPOSCHANGED message to change the display Icon and System buttons on the Menu Bar.

Simulation of Keyboard and Mouse Action

The Menu Bar traps the keyboard by using the _AppKbHookProc() hook function. On keybaord messages, _AppKbHookProc() calls the following:

What Is Called When It Is Called
KeyDown() on WM_KEYDOWN
KeyUp() on WM_KEYUP
SysKeyDown() on WM_SYSKEYDOWN
KeyUp() on WM_SYSKEYUP

These functions will return TRUE if they trap the message; otherwise, FALSE to pass to the default target windows.

To simulate horizontal navigation to the next popup menu of a left or right key press, the _MenuKeyBoardHook() keyboard hook function is called from _AppKbHookProc() if _gs_bMenuKbHooked is set. The _MenuKeyBoardHook() function hides the current menu and shows the next popup menu when there's a left or right press and no changes of menu selection.

Like that, the mouse is also hooked by the _MenuMouseHookProc() function to change popup menus on moving the mouse over the next menu item.

How to Integrate

The Menu Bar can be integreted in three simple steps:

  1. Declare a member m_MenuBar for CMenuBar in CMainFrame.
  2. Create a Menu Bar in CMainFrame::OnCreate(), along with Tool Bars.
  3.     int CMainFrame::OnCreate( LPCREATESTRUCT lpCreateStruct )
        {
            if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1)
                return -1;
    
    
            if( !m_MenuBar.Create( _T("Menu bar") ) )
            {
                TRACE0("Failed to create Menu bar\n");
                return -1;
            }
    
            m_MenuBar.EnableDocking( CBRS_ALIGN_ANY );
    
            ....
    
            EnableDocking(CBRS_ALIGN_ANY);
            DockControlBar( &m_MenuBar );
    
            ....
            ....
    
            return 0;
        }
    
  4. Override CMainFrame::LoadFrame(); call CMenuBar::SubClassMDI() to subclass MainFrame and the MDI Client window.
  5.     BOOL CMainFrame::LoadFrame( UINT nIDResource,
                                    DWORD dwDefaultStyle,
                                    CWnd* pParentWnd,
                                    CCreateContext* pContext )
        {
            if( FALSE == CMDIFrameWnd::LoadFrame( nIDResource,
                                                  dwDefaultStyle,
                                                  pParentWnd,
                                                  pContext ) )
                return FALSE;
    
            m_MenuBar.SubClassMDI( this );
            return TRUE;
        }