Dynamically Build Your Menu and/or Toolbar






3.17/5 (40 votes)
May 12, 2005
5 min read

142237
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.