So I keep coming back to this article on CodeProject:
https://www.codeproject.com/Articles/4758/How-to-customize-the-context-menus-of-a-WebBrowser
I then realised this statement at the top of the article:
The revised sample projects are using a new, much better customization approach that is going to be comprehensively discussed in the next update of this article, which will hopefully be ready in a couple of weeks. I am publishing this semi-documented and not fully-tested code, because I am having indications that some developers may need to have this code much sooner than the day of my next update. For each revised sample there is also a Readme.htm file that briefly describes how the sample works.
I thought I was struggling to understand the code in the article snippets vs the downloaded source! So I read the readme and it stated:
In MFC 7 CHtmlView has embedded support for IDocHostUIHandler, thus I simply override the CHtmlView::OnShowContextMenu method and afterwards I call the ::CustomShowContextMenu() function, (inside CustomMenus.cpp) which works like described in the section 5 of my original article.
So, I decided to add my own function override in my project:
HRESULT CChristianLifeMinistryHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
return CustomContextMenu(ppt, pcmdtReserved);
}
And I added the similar custom menu function:
HRESULT CustomContextMenu(POINT* ppt, IUnknown* pcmdtReserved)
{
IOleWindow* oleWnd = NULL;
HWND hwnd = NULL;
HMENU hMainMenu = NULL;
HMENU hPopupMenu = NULL;
HRESULT hr = 0;
INT iSelection = 0;
if ((ppt == NULL) || (pcmdtReserved == NULL))
goto error;
hr = pcmdtReserved->QueryInterface(IID_IOleWindow, (void**)&oleWnd);
if ((hr != S_OK) || (oleWnd == NULL))
goto error;
hr = oleWnd->GetWindow(&hwnd);
if ((hr != S_OK) || (hwnd == NULL))
goto error;
hMainMenu = LoadMenu(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDR_MENU_HTML_POPUP));
if (hMainMenu == NULL)
goto error;
hPopupMenu = GetSubMenu(hMainMenu, 0);
if (hPopupMenu == NULL)
goto error;
// Show shortcut menu
iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Send selected shortcut menu item command to shell
if (iSelection != 0)
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
error:
if (hMainMenu != NULL)
::DestroyMenu(hMainMenu);
return S_OK;
}
Finally, I added a menu resource:
IDR_MENU_HTML_POPUP MENU
BEGIN
POPUP "CustomPopup"
BEGIN
MENUITEM "View Source", 2139
MENUITEM SEPARATOR
MENUITEM "Select All", 31
END
END
The menu ID values are based on the IDM_ versions and they all work.
I then tried to add my own menu item into that list with my own event handler and it shows as disabled.
Is it not possible to add our own menu items on the CHtmlView context menu?
I wanted to add my own menu item "Print Preview" which in turn simply posted a message to my parent "Editor" to simulate clicking "Print Preview" there. But it seems that any custom item that is added to this menu is always greyed out.
If I add a "Print Preview" menu item and give it a value of 2003(IDM_PRINTPREVIEW) it just triggers the original print preview mechanism. And I can't add my own event handler for the same to my CChristianLifeMinistryHtmlView class as it is not honoured.
I found this article which mentions:
Should you choose to replace the standard menu with your own, you can
still append menu extensions to your custom menu. Simply include a
blank IDM_MENUEXT_PLACEHOLDER menu option in your menu definition to
indicate where the custom commands are to be inserted. Menu extensions
are inserted just before this placeholder. You can also add your own
custom command to the standard menu by inserting the menu option
before IDM_MENUEXT_PLACEHOLDER, as shown in the following example.
#define IDM_MENUEXT_PLACEHOLDER 6047
// If the placeholder is gone or was never there, then just exit if
(GetMenuState(hMenu, IDM_MENUEXT_PLACEHOLDER, MF_BYCOMMAND) != (UINT)
-1) { InsertMenu(hMenu, // The Context Menu
IDM_MENUEXT_PLACEHOLDER, // The item to insert before
MF_BYCOMMAND|MF_STRING, // by item ID and str value
IDM_MENUEXT_FIRST__ + nExtCur, // the command ID
(LPTSTR)aOpts[nExtCur].pstrText);// The menu command text
// Remove placeholder DeleteMenu(hMenu, IDM_MENUEXT_PLACEHOLDER,
MF_BYCOMMAND); }
The menu IDs for extensions fall between IDM_MENUEXT_FIRST__ and IDM_MENUEXT_LAST__ for a maximum of 33 custom
commands.
I know I didn't design it right but I added a menu item for the place holder and then another for Print Preview with a menu item id of IDM_MENUEXT_FIRST__. I then added a menu handler to it. The menu item is no longer disabled so that is good. But clicking it does nothing.
This question relates to:
CHtmlView and Print Preview and Context menu
Update
I think I have found a solution and will provide an answer shortly.
I have got to the bottom of this. There were a few key things that I learned along the way.
Concept 1
I was not away of the CHtmlView::OnShowContextMenu function which I needed to implement:
HRESULT CChristianLifeMinistryHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
return CustomContextMenu(ppt, pcmdtReserved);
}
In my defence, the IDE in Visual Studio did not offer it in the list as a possibly override. But it existed non-the-less.
Concept 2
The menu IDs for all custom menu items fall between IDM_MENUEXT_FIRST__ and IDM_MENUEXT_LAST__ for a maximum of 33 custom commands. In my case I manually create some #define values in my resource.h file:
#define CUSTOM_MENU_PRINT_PREVIEW 3700
#define CUSTOM_MENU_EXPORT 3701
Note that I am not happy using literal values. I wish I could use IDM_MENU_EXT_FIRST + 1 etc. for my definitions but I do not know how to do that. Possible?
Concept 3
When you design your custom menu you can infill existing commands with the IDM_XXX values too:
IDR_MENU_HTML_POPUP MENU
BEGIN
POPUP "CustomPopup"
BEGIN
MENUITEM "Select All", 31
MENUITEM SEPARATOR
MENUITEM "Export", CUSTOM_MENU_EXPORT
MENUITEM SEPARATOR
MENUITEM "Page Setup", 2004
MENUITEM "Print Preview", CUSTOM_MENU_PRINT_PREVIEW
MENUITEM SEPARATOR
MENUITEM "Refresh", 2300
MENUITEM SEPARATOR
MENUITEM "View Source", 2139
END
END
The same note still applies though. I can't work out how to assign the IDM_* constants to my own #defines rather than using literal values.
Your custom menu items will be greyed out if you don't use the right ID values.
Concept 4
Then you have to create the context menu (see original question for that code).
Concept 5
This is closely related to item 4. TrackMenuPopup needed to be adjusted in my situation as follows:
// Show shortcut menu
iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Send selected shortcut menu item command to shell
if (iSelection != 0)
{
if (iSelection == CUSTOM_MENU_PRINT_PREVIEW)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_PRINTPREVIEW, NULL);
}
else if (iSelection == CUSTOM_MENU_EXPORT)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_EXPORT, NULL);
}
else
{
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
}
The key was testing the return value of TrackMenuPopup and doing custom handling. In-fact, whilst writing this answer I now realise that my "Print Preview" menu item can be defined as the value of IDM_PRINTPREVIEW and I test again that, like this:
if (iSelection != 0)
{
if (iSelection == IDM_PRINTPREVIEW)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_PRINTPREVIEW, NULL);
}
else if (iSelection == CUSTOM_MENU_EXPORT)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_EXPORT, NULL);
}
else
{
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
}
So my only gripe is that my own #define are values are using literal numbers and I would prefer them to be based on the IDM constants if possible. But either way, it all works fine!
Related
I'm learning mfc, activex controls and containers. I'm doing a assignment on containers(Dialog).I have created a dialog, when I do right click, there are system menu items like Minimize, Maximize, Move, Size. All these items appeared by selecting true in dialog properties. I have added another item "Always on top" which will be appear on top. When the user click on Always on top I provided a message box which will say "Do you want enable?" I gave yes or no for that, but I have to avoid this message box and need to add a checkbox beside the Always on top item. So that whenever user wants the dialog to appear on top they can use the checkbox.
I have tried in one possible way by adding a bitmap image of checkmark but somehow it's not working and I have less time. If anyone has any idea how I can add this checkbox please let me know.
This is the code where I have appended the item
BOOL CDiagDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
CMenu * pSystemMenu = GetSystemMenu(FALSE);
pSystemMenu->AppendMenu(MF_ENABLED, IDM_SYSCOMMAND_CUSTOM,_T("Always on top"));
return TRUE; // return TRUE unless you set the focus to a control
}
void CDiagDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if (nID == IDM_SYSCOMMAND_CUSTOM)
{
AfxMessageBox(
_T("Enable Always on top"), MB_YESNO
);
switch (iResponse) {
case IDYES:
SetWindowPos(&CWnd::wndTopMost, 0, 0, 0, 0, SWP_NOSIZE);
break;
case IDNO:
SetWindowPos(&CWnd::wndNoTopMost, 0, 0, 0, 0, SWP_NOSIZE);
break;
}
}
else
{
CDialog::OnSysCommand(nID, lParam);
}
}
So I keep coming back to this article on CodeProject:
https://www.codeproject.com/Articles/4758/How-to-customize-the-context-menus-of-a-WebBrowser
I then realised this statement at the top of the article:
The revised sample projects are using a new, much better customization approach that is going to be comprehensively discussed in the next update of this article, which will hopefully be ready in a couple of weeks. I am publishing this semi-documented and not fully-tested code, because I am having indications that some developers may need to have this code much sooner than the day of my next update. For each revised sample there is also a Readme.htm file that briefly describes how the sample works.
I thought I was struggling to understand the code in the article snippets vs the downloaded source! So I read the readme and it stated:
In MFC 7 CHtmlView has embedded support for IDocHostUIHandler, thus I simply override the CHtmlView::OnShowContextMenu method and afterwards I call the ::CustomShowContextMenu() function, (inside CustomMenus.cpp) which works like described in the section 5 of my original article.
So, I decided to add my own function override in my project:
HRESULT CChristianLifeMinistryHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
return CustomContextMenu(ppt, pcmdtReserved);
}
And I added the similar custom menu function:
HRESULT CustomContextMenu(POINT* ppt, IUnknown* pcmdtReserved)
{
IOleWindow* oleWnd = NULL;
HWND hwnd = NULL;
HMENU hMainMenu = NULL;
HMENU hPopupMenu = NULL;
HRESULT hr = 0;
INT iSelection = 0;
if ((ppt == NULL) || (pcmdtReserved == NULL))
goto error;
hr = pcmdtReserved->QueryInterface(IID_IOleWindow, (void**)&oleWnd);
if ((hr != S_OK) || (oleWnd == NULL))
goto error;
hr = oleWnd->GetWindow(&hwnd);
if ((hr != S_OK) || (hwnd == NULL))
goto error;
hMainMenu = LoadMenu(AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDR_MENU_HTML_POPUP));
if (hMainMenu == NULL)
goto error;
hPopupMenu = GetSubMenu(hMainMenu, 0);
if (hPopupMenu == NULL)
goto error;
// Show shortcut menu
iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Send selected shortcut menu item command to shell
if (iSelection != 0)
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
error:
if (hMainMenu != NULL)
::DestroyMenu(hMainMenu);
return S_OK;
}
Finally, I added a menu resource:
IDR_MENU_HTML_POPUP MENU
BEGIN
POPUP "CustomPopup"
BEGIN
MENUITEM "View Source", 2139
MENUITEM SEPARATOR
MENUITEM "Select All", 31
END
END
The menu ID values are based on the IDM_ versions and they all work.
I then tried to add my own menu item into that list with my own event handler and it shows as disabled.
Is it not possible to add our own menu items on the CHtmlView context menu?
I wanted to add my own menu item "Print Preview" which in turn simply posted a message to my parent "Editor" to simulate clicking "Print Preview" there. But it seems that any custom item that is added to this menu is always greyed out.
If I add a "Print Preview" menu item and give it a value of 2003(IDM_PRINTPREVIEW) it just triggers the original print preview mechanism. And I can't add my own event handler for the same to my CChristianLifeMinistryHtmlView class as it is not honoured.
I found this article which mentions:
Should you choose to replace the standard menu with your own, you can
still append menu extensions to your custom menu. Simply include a
blank IDM_MENUEXT_PLACEHOLDER menu option in your menu definition to
indicate where the custom commands are to be inserted. Menu extensions
are inserted just before this placeholder. You can also add your own
custom command to the standard menu by inserting the menu option
before IDM_MENUEXT_PLACEHOLDER, as shown in the following example.
#define IDM_MENUEXT_PLACEHOLDER 6047
// If the placeholder is gone or was never there, then just exit if
(GetMenuState(hMenu, IDM_MENUEXT_PLACEHOLDER, MF_BYCOMMAND) != (UINT)
-1) { InsertMenu(hMenu, // The Context Menu
IDM_MENUEXT_PLACEHOLDER, // The item to insert before
MF_BYCOMMAND|MF_STRING, // by item ID and str value
IDM_MENUEXT_FIRST__ + nExtCur, // the command ID
(LPTSTR)aOpts[nExtCur].pstrText);// The menu command text
// Remove placeholder DeleteMenu(hMenu, IDM_MENUEXT_PLACEHOLDER,
MF_BYCOMMAND); }
The menu IDs for extensions fall between IDM_MENUEXT_FIRST__ and IDM_MENUEXT_LAST__ for a maximum of 33 custom
commands.
I know I didn't design it right but I added a menu item for the place holder and then another for Print Preview with a menu item id of IDM_MENUEXT_FIRST__. I then added a menu handler to it. The menu item is no longer disabled so that is good. But clicking it does nothing.
This question relates to:
CHtmlView and Print Preview and Context menu
Update
I think I have found a solution and will provide an answer shortly.
I have got to the bottom of this. There were a few key things that I learned along the way.
Concept 1
I was not away of the CHtmlView::OnShowContextMenu function which I needed to implement:
HRESULT CChristianLifeMinistryHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
return CustomContextMenu(ppt, pcmdtReserved);
}
In my defence, the IDE in Visual Studio did not offer it in the list as a possibly override. But it existed non-the-less.
Concept 2
The menu IDs for all custom menu items fall between IDM_MENUEXT_FIRST__ and IDM_MENUEXT_LAST__ for a maximum of 33 custom commands. In my case I manually create some #define values in my resource.h file:
#define CUSTOM_MENU_PRINT_PREVIEW 3700
#define CUSTOM_MENU_EXPORT 3701
Note that I am not happy using literal values. I wish I could use IDM_MENU_EXT_FIRST + 1 etc. for my definitions but I do not know how to do that. Possible?
Concept 3
When you design your custom menu you can infill existing commands with the IDM_XXX values too:
IDR_MENU_HTML_POPUP MENU
BEGIN
POPUP "CustomPopup"
BEGIN
MENUITEM "Select All", 31
MENUITEM SEPARATOR
MENUITEM "Export", CUSTOM_MENU_EXPORT
MENUITEM SEPARATOR
MENUITEM "Page Setup", 2004
MENUITEM "Print Preview", CUSTOM_MENU_PRINT_PREVIEW
MENUITEM SEPARATOR
MENUITEM "Refresh", 2300
MENUITEM SEPARATOR
MENUITEM "View Source", 2139
END
END
The same note still applies though. I can't work out how to assign the IDM_* constants to my own #defines rather than using literal values.
Your custom menu items will be greyed out if you don't use the right ID values.
Concept 4
Then you have to create the context menu (see original question for that code).
Concept 5
This is closely related to item 4. TrackMenuPopup needed to be adjusted in my situation as follows:
// Show shortcut menu
iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Send selected shortcut menu item command to shell
if (iSelection != 0)
{
if (iSelection == CUSTOM_MENU_PRINT_PREVIEW)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_PRINTPREVIEW, NULL);
}
else if (iSelection == CUSTOM_MENU_EXPORT)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_EXPORT, NULL);
}
else
{
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
}
The key was testing the return value of TrackMenuPopup and doing custom handling. In-fact, whilst writing this answer I now realise that my "Print Preview" menu item can be defined as the value of IDM_PRINTPREVIEW and I test again that, like this:
if (iSelection != 0)
{
if (iSelection == IDM_PRINTPREVIEW)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_PRINTPREVIEW, NULL);
}
else if (iSelection == CUSTOM_MENU_EXPORT)
{
::SendMessage(GetParent()->GetSafeHwnd(), WM_COMMAND, ID_FILE_EXPORT, NULL);
}
else
{
(void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
}
So my only gripe is that my own #define are values are using literal numbers and I would prefer them to be based on the IDM constants if possible. But either way, it all works fine!
Imagine there is a web page with a <input type="file" />. I open this page in Firefox, and click on the file button. Then a dialog will popup.
I want to set the file name edit of the popup dialog programmatically in C++:
First I use Spy++ to check window class, the Firefox window and popup dialog properties in Spy++ look like:
Firefox window:
Handle: 001E013E
Caption: Table with objects - Mozilla Firefox
Class: MozillaWindowClass
Popup dialog:
Handle: 004508BE
Caption: File Upload
Class: #32770 (Dialog)
Spy++ also shows browser window is the parent of popup dialog.
My code looks like:
#include <Windows.h>
#include <stdio.h>
int main()
{
HWND hBrowser = FindWindow(L"MozillaWindowClass", NULL);
printf("Browser hwnd=%X\n", hBrowser);
HWND hDialog = FindWindowEx(hBrowser, NULL, L"#32770 (Dialog)", NULL);
printf("Dialog hwnd=%X\n", hDialog);
system("pause");
}
But the value of hBrowser does not equal the value in Spy++ dialog, and value of hDialog is NULL. I have only one Firefox window opened, with only one tab.
Then I tried to change my code to:
// 0x001E013E is the handle in Spy++
HWND hDialog = FindWindowEx((HWND)0x001E013E, NULL, L"#32770 (Dialog)", NULL);
hDialog still outputs as NULL.
Questions:
Why the handle in Spy++ and the one I get in program is not the same?
Does the "parent window" in Spy++ have the same meaning with the parent parameter in FindWindowEx?
Note: I can't use window title to do the find, due to localization issue (Firefox may be installed in languages other than English).
MozillaWindowClass is the owner of the open dialog, it is not a parent you can use with FindWindowEx. There can also be more than one MozillaWindowClass window so it is better to look for the dialog first:
BOOL CALLBACK FindMozillaOpenFilenameDialogEnumProc(HWND hWnd, LPARAM param)
{
HWND*pData = (HWND*) param;
if (GetClassLongPtr(hWnd, GCW_ATOM) == 32770) // Found a dialog?
{
HWND hOwner = GetWindow(hWnd, GW_OWNER);
if (hOwner)
{
WCHAR buf[100];
GetClassName(hOwner, buf, 100);
if (0 == lstrcmp(buf, TEXT("MozillaWindowClass"))) // The dialog is owned by a Mozilla window?
{
HWND hCombo = GetDlgItem(hWnd, 0x047c); // cmb13
GetClassName(hCombo, buf, 100);
buf[8] = '\0'; // Some Windows versions use ComboBox and some use ComboBoxEx32, we only care if it is some type of combobox
if (0 == lstrcmp(buf, TEXT("ComboBox"))) // The dialog contains a ComboBox with the expected ID?
{
*pData = hWnd;
return false;
}
}
}
}
return true;
}
int main()
{
HWND hDialog = NULL;
EnumWindows(FindMozillaOpenFilenameDialogEnumProc, (LPARAM) &hDialog);
printf("Dialog hwnd=%X\n", hDialog);
if (hDialog)
{
HWND hCombo = GetDlgItem(hDialog, 0x047c);
SendMessage(hCombo, WM_SETTEXT, 0, (LPARAM) TEXT("c:\\foo\\bar.exe")); // Could also use CDM_SETCONTROLTEXT?
}
return 0;
}
This code relies on undocumented and internal names and window relationships, it could break at any time.
Keep in mind that "MozillaWindowClass" is a internal Mozilla name and could change at any time. The documented cmb13 id of the filename control is only documented for GetOpenFileName and GetSaveFileName but not for IFileDialog based dialogs. You really should use UI Automation when you interact with the open dialog in another application!
I have a popup menu I would like to modify before it is being displayed. I can actually modify the string of a menu item fine. The problem is, that this renders it useless as nothing happens when the modified menu item is clicked on.
CMenu* pPopup = menu.GetSubMenu(0);
ASSERT(pPopup != NULL);
CWnd* pWndPopupOwner = this;
while(pWndPopupOwner->GetStyle() & WS_CHILD)
pWndPopupOwner = pWndPopupOwner->GetParent();
// modify string
pPopup->ModifyMenu(1, MF_BYPOSITION | MF_STRING, NULL, oss.str().c_str());
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, pWndPopupOwner);
Your call to ModifyMenu is setting the menu's ID to zero (via the third argument). You need to keep that ID the same.
If that's awkward, the SetMenuItemInfo API lets you change the string without changing the ID.
I'd like to implement a function with the prototype
/* Locates the menu item of the application which caused the given menu 'mnu' to
* show up.
* #return true if the given menu 'mnu' was opened by another menu item, false
* if not.
*/
bool getParentMenuItem( HMENU mnu, HMENU *parentMenu, int *parentMenuIdx );
Given a HMENU handle, I'd like to be able to find out which menu item (if any) in the application opened it. This is basically the reverse of the GetSubMenu function.
My current approach is to look into each HMENU of the top level windows of the application and check for whether I can find a menu item which would open the given sub menu when activated. I do this recursively, using GetMenuItemCount/GetSubMenu.
This is rather inefficient though, and it fails for menus which are opened by context menu items. Hence, I'm wondering:
Does anybody have a nice idea how to find the menu item (if any) which opens a given HMENU when activated?
UPDATE: An idea which just came to my mind; it should be possible (using the SetWindowsHookEx function) to install a hook which gets notified of all input events which happened in a menu. Whenever a menu item activation is detected, memorize the menu item (identified by a (HMENU,int) pair) and the HMENU which will get opened by the menu item in a global map. The getParentMenuItem function above could then simply perform a lookup into the map.
UPDATE to the update: The hooking idea described in the update above won't work as it is since it will of course only recognize menu item -> menu associations for items which have been activated at some point.
This feels a bit ugly though since it reqiures me to keep a lot of state (the map); are there any easier possibilities?
You could try setting MENUINFO.dwMenuData to the parent menu handle for all menus you create in your application:
MENUINFO mi;
mi.cbSize = sizeof(MENUINFO);
mi.dwMenuData = (ULONG_PTR)<parent HMENU if this is a sub menu>
mi.fMask = MIM_MENUDATA;
SetMenuInfo(hCreatedMenu, &mi);
Then you only need to query this dwMenuData field in your function:
bool getParentMenuItem(HMENU mnu, HMENU *parentMenu, int *parentMenuIdx)
{
MENUINFO mi;
mi.cbSize = sizeof(MENUINFO);
mi.fMask = MIM_MENUDATA;
if (!GetMenuInfo(mnu,&mi) || mi.dwMenuData == 0)
return false;
*parentMenu = (HMENU)mi.dwMenuData;
// not sure how or why you need the parentMenuIdx, but you should be able
// to derive that from the parent HMENU
return true;
}
Edit: If you don't have control over how all menus are created, you could use a WH_CALLWNDPROC hook to trap when a menu is first created. A good article (with source code) describes how this can be done - you could then look at trying to inject the parent HMENU into the created menu using the method described above.
I just found the same need. I have my menus defined in the .rc file, and I want to gray out access to a popup menu if all of its subitems get grayed out. (You can argue that this inhibits discovery, but, in this particular case, it's what we need).
As previous responders mentioned, if you are creating menus programmatically, you can store an item's parent menu handle as ancillary information.
But for menus defined in the resource file using the POPUP keyword, you can't associate an ID with a POPUP and you can't easily climb up the menu tree programmatically. You have to recursively search down for a menu item, and keep track of the parents.
I wrote the following code to do this. EnableSubmenuItem works like EnableMenuItem to enable or disable a menu item by ID. It then checks the item's parent menu. If all items in its parent menu are disabled, the parent gets disabled. Conversely if any subitem is enabled, the parent gets enabled.
(BTW the original question, how to find parent hMenu of a menu item, is addressed by routine FindParentMenu, in the following code).
////////////////////////////////////////////////////////////////////////////////
// MenuContainsID - return TRUE if menu hMenu contains an item with specified ID
////////////////////////////////////////////////////////////////////////////////
static BOOL MenuContainsID (HMENU hMenu, UINT id)
{
int pos; // use signed int so we can count down and detect passing 0
MENUITEMINFO mf;
ZeroMemory(&mf, sizeof(mf)); // request just item ID
mf.cbSize = sizeof(mf);
mf.fMask = MIIM_ID;
for (pos = GetMenuItemCount(hMenu); --pos >= 0; ) // enumerate menu items
if (GetMenuItemInfo(hMenu, (UINT) pos, TRUE, &mf)) // if we find the ID we are looking for return TRUE
if (mf.wID == id)
return TRUE;
return FALSE;
}
////////////////////////////////////////////////////////////////////////////////
// MenuItemIsSubmenu - returns TRUE if item # pos (position) of menu hMenu is a
// submenu. Sets phSubMenu to menu handle if so
////////////////////////////////////////////////////////////////////////////////
static BOOL MenuItemIsSubmenu (HMENU hMenu, UINT pos, HMENU *phSubMenu)
{
MENUITEMINFO mf;
ZeroMemory(&mf, sizeof(mf)); // request just submenu handle
mf.cbSize = sizeof(mf);
mf.fMask = MIIM_SUBMENU;
if (! GetMenuItemInfo(hMenu, pos, TRUE, &mf)) // failed to get item?
return FALSE;
*phSubMenu = mf.hSubMenu; // pass back by side effect
return (mf.hSubMenu != NULL); // it's a submenu if handle is not NULL
}
////////////////////////////////////////////////////////////////////////////////
// MenuItemIsEnabled - returns true if item # pos (position) of menu hMenu is
// enabled (that is, is not disabled or grayed)
////////////////////////////////////////////////////////////////////////////////
static BOOL MenuItemIsEnabled (HMENU hMenu, UINT pos)
{
return ! (GetMenuState(hMenu, pos, MF_BYPOSITION) & (MF_GRAYED | MF_DISABLED));
}
////////////////////////////////////////////////////////////////////////////////
// FindParentMenu - returns handle of the submenu of menu bar hMenu that contains
// an item with id "id". Position of this submenu is passed by side effect to
// pParentPos, and /its/ parent menu is passed by side effect through phGrandparentMenu.
////////////////////////////////////////////////////////////////////////////////
static HMENU FindParentMenu (HMENU hMenu, UINT id, HMENU *phGrandparentMenu, UINT *pparentPos)
{
int pos; // use signed int so we can count down and detect passing 0
HMENU hSubMenu, hx;
for (pos = GetMenuItemCount(hMenu); --pos >= 0; ) {
if (MenuItemIsSubmenu(hMenu, (UINT) pos, &hSubMenu)) {
if (MenuContainsID(hSubMenu, id)) {
*phGrandparentMenu = hMenu; // set grandparent info by side effect
*pparentPos = (UINT) pos;
return hSubMenu; // we found the item directly
}
if ((hx = FindParentMenu(hSubMenu, id, phGrandparentMenu, pparentPos)) != NULL)
return hx; // we found the item recursively (in a sub-sub menu). It set grandparent info
}
}
return NULL;
}
////////////////////////////////////////////////////////////////////////////////
// AllSubitemsAreDisabled - returns TRUE if all items in a submenu are disabled
////////////////////////////////////////////////////////////////////////////////
static BOOL AllSubitemsAreDisabled (HMENU hMenu)
{
int pos; // use signed int so we can count down and detect passing 0
for (pos = GetMenuItemCount(hMenu); --pos >= 0; )
if (MenuItemIsEnabled(hMenu, (UINT) pos))
return FALSE; // finding one enabled item is enough to stop
return TRUE;
}
////////////////////////////////////////////////////////////////////////////////
// EnableSubMenuItem - like EnableMenuItem, enables or disables a menu item
// by ID (only; not position!) where hMenu is top level menu.
// Added bonus: If the item is in a pop-up menu, and all items in the popup are
// now disabled, we disable the popup menu itself.
//
// Example:
// EnableSubMenuItem(hMainMenu, IDM_CONFIGURE, MF_GRAYED);
//
////////////////////////////////////////////////////////////////////////////////
void EnableSubMenuItem (HMENU hMenu, UINT id, UINT enable)
{
HMENU hParentMenu, hGrandparentMenu;
UINT parentPos;
// EnableMenuItem does its job recursively and takes care of the item
EnableMenuItem(hMenu, id, enable | MF_BYPOSITION);
// But popup menus don't have IDs so we have find the parent popup menu, and its parent (the
// grandparent menu), so we can enable or disable the popup entry by position
if ((hParentMenu = FindParentMenu(hMenu, id, &hGrandparentMenu, &parentPos)) != NULL)
EnableMenuItem(hGrandparentMenu, parentPos,
MF_BYPOSITION | (AllSubitemsAreDisabled(hParentMenu) ? MF_GRAYED : MF_ENABLED));
}