I'm Michael Suodenjoki - a software engineer living in Kgs. Lyngby, north of Copenhagen, Denmark. This is my personal site containing my blog, photos, articles and main interests.
I'm Michael Suodenjoki - a software engineer living in Kgs. Lyngby, north of Copenhagen, Denmark. This is my personal site containing my blog, photos, articles and main interests.
Updated 2015.05.07 18:00 +0200 |
Do you have problems in creating a submenu with menu item images in a context popup menu in MFC ? Well, so had I. This article describes my solution to this. While this works (for me at least), I had hoped it would had been a somewhat easier. Naive me. And yes, MFC is still used ;-)
Recently I've spent considerable time to add updated - full RGB/A - bitmaps/icons to an applications' toolbar buttons and menu items (in newest MFC these are also buttons). While this now function well, there was just one small thing that I also wanted. Namely images in menu items appearing in a dynamic submenu to a context popup menu.
The specific use case here is an application with an explorer like user interface where the user only can create specific kinds of new types of documents in different folders. The context menu contains some static/fixed menu items (commands) and the submenu should contain only the menu items (create document commands) for the documents the user can create. Hence the dynamic nature and hence the need for different menu item images depending on the actual allowed document types.
So the application have a fixed set of commands for a context popup menu but a submenu of common dynamic nature. That is, the submenus' items are created dynamically depending on the current context and I also wanted menu item images in those. Using MFC, especially the classes CMFCPopupMenu and CMFCToolBarMenuButton, this was not easy.
The layout of a sample possible context menu look like below:
Item 1 Item 2 ------ Item 3 > Item A Item B ------- Item 4
In the layout Item 1 and 2 are menu items for fixed commands where as Item- 3, A, B, 4 and separators are added dynamically (depending on the context).
If you've created your application main menu and toolbar and loaded these with initial bitmaps representing the image listed for your toolbar buttons and corresponding menu buttons MFC will "miraculous" itself ensure that all menu items, both in your main menu and in any context menus, are displayed with the appropriate menu item image.
Your code might look like something as illustrated below:
// Setup that we want toolbar images in size 24x24 and menu bar images in size 16x16.
// Note that these sizes should match the height found in respectively IDR_APP_TOOLBAR_24x24x32
// and IDR_APP_TOOLBAR_16x16x32 used below.
CMFCToolBar::SetMenuSizes( CSize( 24, 24 ), CSize( 16, 16 ) );
if( !m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER
| CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
!m_wndToolBar.LoadToolBar( IDR_APP_TOOLBAR_24x24x32, 0, IDR_APP_TOOLBAR_16x16x32 ) )
{
TRACE0("Failed to create toolbar\n");
return -1; // fail to create
}
...
// Load menu item images (not placed on any standard toolbars):
CMFCToolBar::AddToolBarForImageCollection(IDR_MENUTOOLBAR_24x24x32, IDR_MENUTOOLBAR_24x24x32,
0, IDR_MENUTOOLBAR_16x16x32);
Both the IDR_APP_TOOLBAR_24x24x32 and the IDR_MENUTOOLBAR_24x24x32 are resource id's for both a TOOLBAR resource as well for a corresponding bitmap representing the image list of the toolbar button images.
The resource id's IDR_APP_TOOLBAR_16x16x32 and IDR_MENUTOOLBAR_16x16x32 are however representing a corresponding bitmap image list with smaller images used for menus, i.e. 16x16 pixels. Note that none of these might contain the menu item images that you really want to appear in your dynamic submenu of the context popup menu. Your code might not know these yet e.g. because they depend on user actions.
Well at some point in time you want to create and show your context sensitive popup menu with dynamic created submenu. With MFC you create a popup menu by allocating a CMFCPoupMenu and create it, as illustrated below:
CMFCPopupMenu* popup = new CMFCPopupMenu(); // This will auto-delete
popup->InsertItem(CMFCToolBarMenuButton(ID_ITEM1, NULL, -1, EBC_TEXT("Item 1")));
popup->InsertItem(CMFCToolBarMenuButton(ID_ITEM1, NULL, -1, EBC_TEXT("Item 2")));
// Creates the popup and will display it on the screen relative to the parent
// window CWnd wnd at position CPoint pos...
popup->Create(wnd, pos.x, pos.y, NULL);
This establishes the static/fixed part of the popup menu and if the ID_ITEM1 and ID_ITEM2 commands are mapped into any of the toolbar resources IDR_APP_TOOLBAR_24x24x32 or IDR_MENUTOOLBAR_24x24x32 they will appear with a menu item image coming from either IDR_APP_TOOLBAR_16x16x32 or IDR_MENUTOOLBAR_16x16x32.
Now, let me show you the code to add one extra image into the image list used for the menu item images:
unsigned int cmdid = ...
CCommandManager * cmdmgr = GetCmdMgr();
// Check whether the command already have an associated index into the menu image list...
int imgindex = GetCmdMgr()->GetCmdImage(cmdid, FALSE);
if( imgindex == -1 ) // not found
{
// Get the menu image list...
CMFCToolBarImages * images = CMFCToolBar::GetMenuImages();
if( images )
{
// Get the size of the images in the menu image list (e.g. 16x16)
const CSize menuimgsize = CMFCToolBar::GetMenuImageSize();
// Load and image (bitmap or icon, below we use a sample icon file,
// but this can be establish in several ways)
HICON hIcon = (HICON)LoadImage(NULL, EBC_TEXT("C:\\Icons\\itema.ico"), IMAGE_ICON,
menuimgsize.cx, menuimgsize.cy, LR_LOADFROMFILE);
if( hIcon )
{
imgindex = images->AddIcon(hIcon, TRUE);
DestroyIcon(hIcon);
}
}
}
You can imagine that this forms a basis for dynamically adding extra images into the menu image list. You can even call GetCmdMgr()-> SetCmdImage() to change association between commands ids and the index into the image list. You can also use method CMFCToolBarImages::DeleteImage() to remove an image from the image list. Both of these methods are practical if you want to add/remove images in a dynamical fashion. Note however that from a performance point of view you really do not want to load the icons/bitmaps images every time the context menu is to be displayed. Instead you should cache this (which I will not describe here).
Now, lets continue with creating the dynamic part; this extends the code we already worked with:
CMFCPopupMenu* popup = new CMFCPopupMenu(); // This will auto-delete
popup->InsertItem(CMFCToolBarMenuButton(ID_ITEM1, NULL, -1, EBC_TEXT("Item 1")));
popup->InsertItem(CMFCToolBarMenuButton(ID_ITEM1, NULL, -1, EBC_TEXT("Item 2")));
pop->InsertSeparator();
// First create a submenu based on plain CMenu with the menu texts
// (no images are yet specified).
CMenu submenu;
submenu.CreateMenu();
submenu.AppendMenu(MF_ENABLED|MF_STRING, ID_ITEMA, EBC_TEXT("Item A"));
submenu.AppendMenu(MF_ENABLED|MF_STRING, ID_ITEMB, EBC_TEXT("Item B"));
// Create the 'Item 3' menu item which will show the submenu.
CMFCToolBarMenuButton submenubutton((UINT)-1, NULL, -1, EBC_TEXT("Item 3"));
submenubutton.CreateFromMenu(submenu.GetSafeHmenu());
// Setup images
SetButtonImage( submenubutton, ID_ITEMA, imgindex_itema );
SetButtonImage( submenubutton, ID_ITEMB, imgindex_itemb );
// Insert the submenu into the popup menu
pop->InsertItem(submenubutton);
// Continue inserting...
pop->InsertSeparator();
popup->InsertItem(CMFCToolBarMenuButton(ID_ITEM4, NULL, -1, EBC_TEXT("Item 4")));
// Creates the popup menu and will display it on the screen relative to the parent
// window CWnd wnd at position CPoint pos...
popup->Create(wnd, pos.x, pos.y, NULL);
Except for the SetButtonImage() function the above should be understandable. To create the submenu we first create a plain CMenu which is "converted" to a CMFCToolBarButton with the call to CreateFromMenu(). It took me quite some time to understand that this - the single button - will contain several commands inside after CMFCToolBarMenuButton::CreateFromMenu() is called. It confused me and still does.
The SetButtonImage() function uses the commands inside to lookup the relevant command and set the menu item (button) image for it:
void SetButtonImage( CMFCToolBarMenuButton & menubutton, UINT cmdid, int imgindex )
{
const CObList & buttons = menubutton.GetCommands();
for( POSITION pos = buttons.GetHeadPosition(); pos != nullptr; )
{
CMFCToolBarButton * button = DYNAMIC_DOWNCAST(CMFCToolBarButton, buttons.GetNext(pos));
if( button && button->m_nID == cmdid )
button->SetImage(imgindex);
}
}
Well, that is basically it. If anybody have a better way to do this, then I'm interested to hear about it.
PS. Many of the methods called in the sample source code above actually returns results - return values - which in real life code should be checked for errors. To make code easier to understand this has deliberately been left out.