Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Dynamically Build Your Menu and/or Toolbar

12 May 2005 1  
Build a menu and/or a toolbar dynamically without using resource files (well, almost).

Introduction

In the latest series of changes to our flagship application, we had a requirement to dynamically build the program's menu and toolbar from a database source. This article is intended to show the technique of dynamically building a menu and a toolbar without using the resource file any more than necessary, and nothing more. Since everyone's source for their command ID's will probably be different, I left the act of building and accessing the list of menu items from which to build your menu as an exercise for the reader.

Before we begin

Why we did this instead of using resource files is really a moot point, and I won't be fielding questions about why we did this or even the ramifications of doing so. This article exists solely to expose a technique so that you don't have to spend the four hours I needed to solve this particular problem.

Our own application

If your curiosity is getting the better of you, our data source is a single table in an oracle database with the following menu-related structure (some of the columns are omitted because they're not related to the act of building a menu or toolbar):

PARENT_ID NUMBER (0-999) Parent ID of the item.
ITEM_ID NUMBER (0-999) ID of the item (0=separator).
ITEM_ORDER NUMBER (1-999) The order in which the item appears in its sub-menu.
ITEM_TITLE VARCHAR2(255) The menu item description (how it appears in the menu).
HAS_CHILDREN NUMBER (0-1) 0=no, 1=yes - indicates that this item is a popup menu.
ICON_ID NUMBER (1-32535) Used by items that are toolbar buttons to identify the resource ID (in the rc file) of the bitmap associated with this button.
IS_TOOLBAR_BTN NUMBER 0=no, 1=yes - indicates that this menu item is a toolbar item.
TOOLBTN_ORDER NUMBER Specifies the order in which this item is placed on the toolbar (if the item is a toolbar button).
TOOLBTN_TOOLTIP VARCHAR2(255) Tooltip text displayed when mouse hovers over the toolbar button.
TOOLBTN_STATUSBAR VARCHAR2(255) Text displayed on the status bar when mouse hovers over the toolbar button.

We have an additional field that allows us to specify a command that does not show up in the menu/toolbar, yet is available through our command list.

Our table currently contains almost 200 items. We load this table into a CTypedPtrArray of COptions (a class that allows us to set/get properties for each menu/toolbar item) sorted on item_order and then parent_id so that the menu exists in the list in the order in which we want the things to be displayed in the menu and toolbar.

The way you load and maintain your list is completely up to you, as well as the fields that you feel are necessary to make your menu/toolbar function as desired. One thing to consider is the inclusion of separators since most menus and toolbars have them. We assign an Item ID of 0 and set the item_title field to "SEPARATOR" so that they're easy to identify.

One other aspect of this is the idea of command ID's. For our application, we set the item_id field starting at 1 and going up through 999. When we load the items from the database, we add these ID's to a base value of 11000. We use this command ID for several things in our application, and it's especially important while considering the use of tool tips. This also allows us to move the ID range around if we need to, and gives us a known starting/stopping point for the IDs. This in turn allows us to handle all ID's through a single handler function, keeping the message map to a bare minimum. I don't know about you, but I absolutely hate scrolling through a couple of hundred message map entries.

Other architectural aspects of our program (liberal use of extension DLLs) allows us to handle large groups of menu items in a certain way, so that even with 170 menu items in the database, our switch statement contains only a dozen or so case items.

Code snippets

We have a variable in our CMainFrame object that is a pointer to a class that actually loads the menu item list and contains functions to build the menu. For the purposes of example, we'll call this class CMyClass. If you find reference mistakes as far as class or variable names go, please politely point them out, and I will fix them as soon as possible.

//in h file

// this is the cklas that stores an item's properties

class CMenuOption
{
};

class CMyClass
{
public
    CMyClass();
    virtual ~CMyClass();
    // load your list here 

    bool LoadMenuItemList() {};
    CMenu* BuildMenu();
    bool BuildToolBar(CToolBar* pToolBar);
    // iterate thru list to find desired cmd id

    CMenuOption* GetItemByCommandID(nID); {  };

protected:
    CTypedPtrAra<CPtrAray, CMenuOPtion*> m_optionList;
    CMenu m_MainMenu;

    void BuildSubMenu(CMenu* pMenu, long nParentID);
    CMenu* RepopulateSpecificSubMenu();
};

We need a starting point from which to build the menu, so we provide this public function to do the same:

// in cpp file

CMenu* CMyClass::BuildMenu()
{
    m_MainMenu.CreateMenu();
    m_nProfilesPos = -1;
    BuildSubMenu(&m_MainMenu, 0);
    return &m_MainMenu;
}

The actual building of the menu is performed by the following recursive function. Our list object is referred to as m_optionList.

void CMyClass::BuildSubMenu(CMenu* pMenu, long nParentID)
{
    CMenuOption* pItem  = NULL;
    long    nItemParentID;
    long    nItemID;
    long    nCommandID;
    CString sTitle;
    BOOL    bResult    = FALSE;

    // look through all of the menu items

    int nCount = m_optionList.GetCount();
    for (int nID = 1; nID < nCount; nID++)
    {
        pItem          = m_optionList.GetListItem(nID);
        nItemParentID  = pItem->GetParentID();
        nCommandID     = pItem->GetCommandID();
        sTitle         = pItem->GetTitle();

        // if the title says doesn't say "separator", 

        // and if the item ID is 0,

        // we don't need this one either (the only  

        // valid reason for the item 

        // id to be 0 is if the item is a separator)

        if (sTitle.CompareNoCase("SEPARATOR") != 0 && nItemID == 0) 
        {
            continue;
        }

        // if the current item has children, it's a popup menu

        if (pItem->GetHasChildren())
        {
            // create a new popup menu

            CMenu subMenu;
            subMenu.CreatePopupMenu();
            // call this function again

            BuildSubMenu(&subMenu, nItemID);
            // append the menu

            pMenu->AppendMenu(MF_POPUP, (UINT)subMenu.m_hMenu, sTitle);
            // detach the sub menu

            subMenu.Detach();
            // if you need to dynamically re-populate a submenu, store 

            // it's position in the menu at this point. Of course, you 

            // don't have to do that, but it makes the repopulate function 

            // below work that much easier to use in your code.  We only 

            // have one such submenu, otherwise the repopulate fucntion 

            // would be more versatile

        }
        else 
        {
            if (pItem->GetID() == 0)
            {
                // append a separator

                pMenu->AppendMenu(MF_SEPARATOR, 0, "");
            }
            else
            {
                // append a regular menu item

                pMenu->AppendMenu(MF_STRING, nCommandID, sTitle);
            }
        }
    }
}
CMenu* CMyClass::RepopulateSpecificSubMenu()
{
    // if you stored the position while you were 

    // building the menu, you can use 

    // the following code to get a menu handle. 

    // If not, you have to find another 

    // way to get the menu's position (probably a "for" loop).

    CMenu* pSubMenu = m_MainMenu.GetSubMenu(m_nProfilesPos); 
    // now delete all of the existing items

    int nSubCount = pSubMenu->GetMenuItemCount();
    for (int i = nSubCount - 1; i >= 0; i--)
    {
        pSubMenu->DeleteMenu(i, MF_BYPOSITION);
    }

    // call our recursive function to build the 

    // menu all over again the "6" is 

    // the id of the menu item that is the popup menu

    BuildSubMenu(pSubMenu, 6); 

    return &m_MainMenu;
}

The toolbar doesn't need a recursive function, so building it is a simple matter. The only real problem I've found is that there doesn't appear to be a way to set the status bar or tootip text directly into a toolbar, but there is a way to display tool tips (but you have to have them available when you need them). The technique is shown after this part of the article.

bool CMyClass::BuildToolBar(CToolBar* pToolBar)
{
    // set the styles for our toolbar (remember your styel requirements may 

    // be different than what I have to use

    DWORD dwMainToolbarStyle = WS_CHILD      | 
                               WS_VISIBLE    | 
                               CBRS_TOP      | 
                               CBRS_TOOLTIPS | 
                               CBRS_FLYBY    | 
                               CBRS_SIZE_DYNAMIC;
    if (!pToolBar->CreateEx(m_pParentWnd, TBSTYLE_FLAT, dwMainToolbarStyle))
    {
        TRACE0("Failed to create toolbar\n");
        return false;      // fail to create

    }
    // set the button sizes - the sizes below are what we use for our app

    SIZE sz1;
    SIZE sz2;
    sz1.cx = 20; // default toolbar button width is 16

    sz1.cy = 18; // default toolbar button width is 15

    // msdn says to add this much to account for borders and such

    sz2.cx = sz1.cx + 7; 
    sz2.cy = sz1.cy + 6;
    pToolBar->SetSizes(sz2, sz1);

    int     nCount = m_optionList.GetListCount();
    CAppNavOption* pItem  = NULL;
    long    nItemParentID;
    long    nItemID;
    int nIndex = 0;
    int nButtonCnt = 0;
    // we need to get the toolbarctrl so we can 

    // add bitmaps for our buttons. The 

    // bitmaps are stored in the RC file

    CToolBarCtrl& tbCtrl = pToolBar->GetToolBarCtrl();
    int nTemp;
    for (int i = 0; i < nCount; i++)
    {
        pItem = m_optionList.GetListItem(i);
        if (!pItem)
        {
            return false;
        }
        if (pItem->GetIsToolbarButton())
        {
            if (pItem->GetID() != 0)
            {
                nTemp = tbCtrl.AddBitmap(1, pItem->GetIconID());
            }
            nButtonCnt++;
        }
    }
    // now tell the toolbar how many buttons we have

    pToolBar->SetButtons(NULL, nButtonCnt);
    nButtonCnt = -1;

    // and now we actually add the button commands to the toolbar

    for (i = 0; i < nCount; i++)
    {
        pItem = m_optionList.GetListItem(i);
        if (!pItem)
        {
            return false;
        }

        nItemParentID  = pItem->GetParentID();
        nItemID        = pItem->GetID();

        if (pItem->GetID() != 0)
        {
            // separator

            pToolBar->SetButtonInfo(++nButtonCnt, 
                pItem->GetCommandID(), TBBS_BUTTON, nIndex++);
        }
        else
        {
            // button

            pToolBar->SetButtonInfo(++nButtonCnt, 0, 
                                            TBBS_SEPARATOR, 20);
        }
    }
    return true;
}

Now that we have a foundation for creating the menu and toolbar, we can modify CMainFrame to actually use the code. All of the action occurs in the CMainFrame::OnCreate() (where you otherwise normally build your toolbar):

//-------------------------------------------------

//-------------------------------------------------

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CXTFrameWnd::OnCreate(lpCreateStruct) == -1)
    {
        return -1;
    }

    m_pMyMenuBar = new CMyClass();
    if (!m_pMenuBar)
    {
        return -1;
    }
    // tell the menubar class to load the menu 

    // items (and perform any other 

    // initialization your app  might require

    if (!m_pMedbaseMenuBar2->InitMenuBar())
    {
        return -1;
    }

    // allow us to dock the toolbar

    EnableDocking(CBRS_ALIGN_TOP);

    // build the menu

    CMenu* pMainMenu = m_pMedbaseMenuBar2->BuildMenu();
    if (!pMainMenu)
    {
        return -1;
    }

    // Get rid of the original default menu

    ::SetMenu(this->GetSafeHwnd(), NULL);
    ::DestroyMenu(m_hMenuDefault);
    // set the app's menu to the one we just built

    SetMenu(pMainMenu);
    m_hMenuDefault = pMainMenu->GetSafeHmenu();

    // make room for it on the frame

    RecalcLayout(TRUE);

    // build the toolbar from the database

    if (!m_pMedbaseMenuBar2->BuildToolBar(&m_wndToolBar))
    {
        return -1;
    }
    // allow the user to see tooltips

    m_wndToolBar.EnableToolTips(TRUE);

    //dock the toolbar

    m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
    DockControlBar(&m_wndToolBar, AFX_IDW_DOCKBAR_TOP);

    return 0;
}

As you can see, it's pretty clean from the outside since most of the dirty details are hidden in the CMyClass object. Next, we need to add a handler for the tooltips - all we have to do is to add a message map entry for the TTN_NEEDTEXT message, and a function to do the handling:

// in the cpp file

BEGIN_MESSAGE_MAP(CMainFrame, CXTFrameWnd)
    //{{AFX_MSG_MAP(CMainFrame)

    ON_WM_CREATE()
    //}}AFX_MSG_MAP

    ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnShowTooltips)
END_MESSAGE_MAP()

BOOL CMainFrame::OnShowTooltips(UINT id, 
           NMHDR *pNMHDR, LRESULT *pResult)
{
    TOOLTIPTEXT* pTTT = (TOOLTIPTEXT*)pNMHDR;
    UINT nID = pNMHDR->idFrom;
    // the function we use to get the command ID 

    // simply iterates through our list 

    // and looks for the command ID of the 

    // toolbar item we're hovering over.

    CAppNavOption* pOption = 
       m_pMenuBar->GetItemByCommandID(nID, true, true);
    if (pOption)
    {
        CString sToolTip = pOption->GetToolBarToolTip();
        if (!sToolTip.IsEmpty())
        {
            strcpy(pTTT->szText, (LPCTSTR)sToolTip);
            return TRUE;
        }
    }
    return FALSE;
}

In our app, the message map contains just 14 items, of which six are standard MFC CWND overrides, seven are registered window messages, and one is the ON_COMMAND_RANGE handler for the menu and toolbar commands.

Conclusion

Once again, this is an overview of a technique and due to its very nature, I can't get any more specific than showing you how to recursively build your menu, change the contents of a specific submenu, and build your toolbar from a given data source. That's why there aren't any sample files. While you still have to spend the time to actually write code to build your list, you should be able to make the necessary changes to the code snippets I've provided here to do whatever you need to do concerning your menu and toolbar.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here