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.
class CMenuOption
{
};
class CMyClass
{
public
CMyClass();
virtual ~CMyClass();
bool LoadMenuItemList() {};
CMenu* BuildMenu();
bool BuildToolBar(CToolBar* pToolBar);
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:
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;
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 (sTitle.CompareNoCase("SEPARATOR") != 0 && nItemID == 0)
{
continue;
}
if (pItem->GetHasChildren())
{
CMenu subMenu;
subMenu.CreatePopupMenu();
BuildSubMenu(&subMenu, nItemID);
pMenu->AppendMenu(MF_POPUP, (UINT)subMenu.m_hMenu, sTitle);
subMenu.Detach();
}
else
{
if (pItem->GetID() == 0)
{
pMenu->AppendMenu(MF_SEPARATOR, 0, "");
}
else
{
pMenu->AppendMenu(MF_STRING, nCommandID, sTitle);
}
}
}
}
CMenu* CMyClass::RepopulateSpecificSubMenu()
{
CMenu* pSubMenu = m_MainMenu.GetSubMenu(m_nProfilesPos);
int nSubCount = pSubMenu->GetMenuItemCount();
for (int i = nSubCount - 1; i >= 0; i--)
{
pSubMenu->DeleteMenu(i, MF_BYPOSITION);
}
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)
{
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;
}
SIZE sz1;
SIZE sz2;
sz1.cx = 20;
sz1.cy = 18;
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;
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++;
}
}
pToolBar->SetButtons(NULL, nButtonCnt);
nButtonCnt = -1;
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)
{
pToolBar->SetButtonInfo(++nButtonCnt,
pItem->GetCommandID(), TBBS_BUTTON, nIndex++);
}
else
{
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;
}
if (!m_pMedbaseMenuBar2->InitMenuBar())
{
return -1;
}
EnableDocking(CBRS_ALIGN_TOP);
CMenu* pMainMenu = m_pMedbaseMenuBar2->BuildMenu();
if (!pMainMenu)
{
return -1;
}
::SetMenu(this->GetSafeHwnd(), NULL);
::DestroyMenu(m_hMenuDefault);
SetMenu(pMainMenu);
m_hMenuDefault = pMainMenu->GetSafeHmenu();
RecalcLayout(TRUE);
if (!m_pMedbaseMenuBar2->BuildToolBar(&m_wndToolBar))
{
return -1;
}
m_wndToolBar.EnableToolTips(TRUE);
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:
BEGIN_MESSAGE_MAP(CMainFrame, CXTFrameWnd)
ON_WM_CREATE()
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;
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.