Click here to Skip to main content
15,885,757 members
Articles / Desktop Programming / MFC
Article

Dynamically Build Your Menu and/or Toolbar

Rate me:
Please Sign up or sign in to vote.
3.18/5 (40 votes)
12 May 20055 min read 139.8K   39   27
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_IDNUMBER (0-999)Parent ID of the item.
ITEM_IDNUMBER (0-999)ID of the item (0=separator).
ITEM_ORDERNUMBER (1-999)The order in which the item appears in its sub-menu.
<fontsize="-1">ITEM_TITLEVARCHAR2(255)The menu item description (how it appears in the menu).
HAS_CHILDRENNUMBER (0-1)0=no, 1=yes - indicates that this item is a popup menu.
ICON_IDNUMBER (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_BTNNUMBER0=no, 1=yes - indicates that this menu item is a toolbar item.
TOOLBTN_ORDERNUMBERSpecifies the order in which this item is placed on the toolbar (if the item is a toolbar button).
TOOLBTN_TOOLTIPVARCHAR2(255)Tooltip text displayed when mouse hovers over the toolbar button.
TOOLBTN_STATUSBARVARCHAR2(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


Written By
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
Generalinactive popup menu Pin
yenh411-Jan-07 15:22
yenh411-Jan-07 15:22 
GeneralRe: inactive popup menu Pin
#realJSOP12-Jan-07 12:01
mve#realJSOP12-Jan-07 12:01 
GeneralAbout CAppNavOption class Pin
happycode199722-Oct-06 21:36
happycode199722-Oct-06 21:36 
GeneralRe: About CAppNavOption class Pin
#realJSOP23-Oct-06 12:56
mve#realJSOP23-Oct-06 12:56 
Questionhow add winhelp ?? Pin
Lucecilla18-Dec-05 23:43
Lucecilla18-Dec-05 23:43 
AnswerRe: how add winhelp ?? Pin
#realJSOP19-Dec-05 12:44
mve#realJSOP19-Dec-05 12:44 
GeneralHey Dave O'Neill Pin
#realJSOP12-May-05 10:08
mve#realJSOP12-May-05 10:08 
GeneralRe: Hey Dave O'Neill Pin
David O'Neil12-May-05 10:46
professionalDavid O'Neil12-May-05 10:46 
GeneralThe Same can be done in XML Pin
Jerry Walter12-May-05 4:24
Jerry Walter12-May-05 4:24 
GeneralRe: The Same can be done in XML Pin
#realJSOP12-May-05 4:32
mve#realJSOP12-May-05 4:32 
GeneralI see Pin
Anonymous12-May-05 4:23
Anonymous12-May-05 4:23 
GeneralRe: I see Pin
#realJSOP12-May-05 4:33
mve#realJSOP12-May-05 4:33 
GeneralRe: I see Pin
Jim Crafton12-May-05 5:08
Jim Crafton12-May-05 5:08 
GeneralRe: I see Pin
#realJSOP12-May-05 5:22
mve#realJSOP12-May-05 5:22 
GeneralRe: I see Pin
Jim Crafton12-May-05 5:30
Jim Crafton12-May-05 5:30 
GeneralRe: I see Pin
#realJSOP12-May-05 5:59
mve#realJSOP12-May-05 5:59 
GeneralRe: I see Pin
Jim Crafton12-May-05 9:31
Jim Crafton12-May-05 9:31 
GeneralRe: I see Pin
Anonymous12-May-05 5:19
Anonymous12-May-05 5:19 
GeneralRe: I see Pin
Jim Crafton12-May-05 5:28
Jim Crafton12-May-05 5:28 
GeneralRe: I see Pin
Anonymous12-May-05 6:04
Anonymous12-May-05 6:04 
GeneralRe: I see Pin
#realJSOP12-May-05 6:23
mve#realJSOP12-May-05 6:23 
GeneralRe: I see Pin
#realJSOP12-May-05 6:21
mve#realJSOP12-May-05 6:21 
GeneralRe: I see Pin
Anonymous12-May-05 8:37
Anonymous12-May-05 8:37 
GeneralRe: I see Pin
bolivar12312-May-05 6:53
bolivar12312-May-05 6:53 
GeneralRe: I see Pin
DaTxomin12-May-05 7:42
DaTxomin12-May-05 7:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.