How do I create a dynamic menu for a ribbon button? - mfc

I know how to generate a static menu item for a button with the following code:
CBCGPRibbonCategory *pCategory = ...;
CBCGPRibbonPanel *pPanel = pCategory->AddPanel(_T("Panel Name"), hPanelIcon);
HMENU hMenu = CreateMenu();
AppendMenu(hMenu, MF_STRING, ID_ITEM_1, _T("Item 1"));
AppendMenu(hMenu, MF_STRING, ID_ITEM_2, _T("Item 2"));
CBCGPRibbonButton * pButton = new CBCGPRibbonButton(ID_BUTTON_WITH_MENU, _T(""))
pButton->SetMenu(hMenu, TRUE); // for dynamic
pPanel->Add(pButton);
But if I want the menu to be dynamic, what do I do? I tried to modify the original HMENU by overriding the OnShowPopupMenu() and changing the HMENU that I attached to that button, but that didn't work.
I was able to force a menu under the button by creating a menu and using the TrackPopupMenu() function but the style is wrong. (Looks grey instead of white and some other differences).
EDIT
To solve this, I did the following:
class CDynamicMenuButton : public CBCGPRibbonButton
{
public:
CDynamicMenuButton(
UINT nID,
LPCTSTR lpszText,
int nSmallImageIndex = -1,
int nLargeImageIndex = -1,
BOOL bAlwaysShowDescription = FALSE)
: CBCGPRibbonButton(nID, lpszText, nSmallImageIndex, nLargeImageIndex, bAlwaysShowDescription)
{
SetMenu(CreatePopupMenu());
}
void OnShowPopupMenu() override
{
// legacy code to generate menu
CMenu newMenu;
populateMenu(&newMenu);
// sets the new menu
SetMenu(newMenu);
// Pops up the new menu
CBCGPRibbonButton::OnShowPopupMenu();
}
};
Adding this button to a panel will then generate the dynamic menu that I want.

The ribbon seems to only use the hMenu as a template to build it's own structure from it, so modifying the hMenu is vain. Better work with the existing Ribbon Menu Button:
pBtn->RemoveAllSubItems(); // add a dummy hMenu when creating the menu button in CMainFrame!
std::auto_ptr<CMFCRibbonButtonEx> apBtn3(new CMFCRibbonButtonEx(ID_DYNAMIC_MENU_ITEM_3, "Item 3", -1, -1, true));
pBtn->AddSubItem(apBtn.release());
std::auto_ptr<CMFCRibbonButtonEx> apBtn4(new CMFCRibbonButtonEx(ID_DYNAMIC_MENU_ITEM_4, "Item 4", -1, -1, true));
pBtn->AddSubItem(apBtn.release());
But make sure to update the menu at the right place in your code. Changing the menu in CMyView::OnUpdate() proved to be not such a good idea (see here). If you need to modify the menu when opening a mdi document, consider OnInitialUpdate(). I haven't tried OnCmdMsg() yet.
Maybe its sufficient to get pBtn via CMFCRibbonBar::FindByID() but maybe its the right thing to iterate through CMFCRibbonBar::GetElementsByID and change each menu button you find (i.e. to also modify the quick access toolbar?)... I found the documentation is not very specific about that but modifying via ´FindByID´ seems to be sufficient in my code.
If you have any further revelations about dynamic ribbon menus, please leave me a comment.

Related

MFC OnMeasureItem & OnDrawItem in menu of MDI multidoc application

(Update, see original question below)
After doing a bit of digging, I'm basically trying to understand the following; In the context of an MDI application, if a menu (which is associated with a specific CChildWnd) has an MF_OWNERDRAW, why are the ON_WM_MEASUREITEM and ON_WM_DRAWITEM events send to the CMainWnd instead of the CChildWnd?
In my InitInstance, the document template is registered and the associated menu is modified to add the MF_OWNERDRAW:
BOOL CMyApp::InitInstance()
{
// ...
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_CHILDFRAME,
RUNTIME_CLASS(CFooDoc),
RUNTIME_CLASS(CFooWnd),
RUNTIME_CLASS(CFooView)
);
if (pDocTemplate->m_hMenuShared != NULL) {
CMenu* pMenu = CMenu::FromHandle(pDocTemplate->m_hMenuShared);
// Add MF_ONWERDRAW to the items that need it.
pMenu->ModifyMenu([item_id], MF_BYCOMMAND | MF_OWNERDRAW, [item_id]);
}
AddDocTemplate(pDocTemplate);
// ...
}
So, once the document template is registered, the menu associated with the document/frame is modified to add the MF_ONWERDRAW flag to each of the required items (the color selection items in my case).
However, why are the OnMeasureItem and OnDrawItem events send to the CMainWnd and not the CFooWnd? And how can I direct the events to the CFooWnd instead?
The reason I'am asking, if I have 5 different types of documents in my MDI application, each needing custom menus, then the CMainWnd basically becomes a mess of message handling. The logical place for the custom menu logic is in the CChildWnd, not the CMainWnd.
Original question:
I'm doing some work on a very old application (MFC 4.2) and I'm running into a problem with drawing in a menu item.
The original application has a menu to select a color and it actually draws the colors in the menu when opened so it easier for the user to select the color.
The behavior for this implemented in CMainWnd using the OnMeasureItem and the OnDrawItem.
class CMainWnd : public CMDIFrameWnd
{
DECLARE_DYNCREATE(CMainWnd)
protected:
afx_msg void OnMeasureItem(int, LPMEASUREITEMSTRUCT);
afx_msg void OnDrawItem(int, LPDRAWITEMSTRUCT);
DECLARE_MESSAGE_MAP()
};
Then, in the implementation (omitted bits and pieces for brevity):
BEGIN_MESSAGE_MAP(CMainWnd, CMDIFrameWnd)
ON_WM_MEASUREITEM()
ON_WM_DRAWITEM()
END_MESSAGE_MAP()
void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
lpmis->itemWidth = ::GetSystemMetrics(SM_CYMENU) * 4;
lpmis->itemHeight = ::GetSystemMetrics(SM_CYMENU) * 1;
}
void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
CDC dc;
dc.Attach(lpdis->hDC);
CBrush* pBrush;
// draw the hover/selection rectangle
pBrush = new CBrush(::GetSysColor((lpdis->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHT :
COLOR_MENU));
dc.FrameRect(&(lpdis->rcItem), pBrush);
delete pBrush;
// load a checkbox icon into a bitmap
BITMAP bm;
CBitmap bitmap;
bitmap.LoadOEMBitmap(OBM_CHECK);
bitmap.GetObject(sizeof(bm), &bm);
// if color/item selected then draw the checkbox
if (lpdis->itemState & ODS_CHECKED) {
CDC dcMem;
dcMem.CreateCompatibleDC(&dc);
CBitmap* pOldBitmap = dcMem.SelectObject(&bitmap);
dc.BitBlt(
lpdis->rcItem.left + 4,
lpdis->rcItem.top + (((lpdis->rcItem.bottom - lpdis->rcItem.top) - bm.bmHeight) / bm.bmWidth,
bm.bmHeight,
&dcMem,
0,
0,
SRCCOPY
);
dcMem.SelectObject(pOldBitmap);
}
// draw the actual color bar
pBrush = new CBrush(CPaintDoc::m_crColors[lpdis->itemID - ID_COLOR_BLACK]);
CRect rect = lpdis->rcItem;
rect.DeflateRect(6, 4);
rect.left += bm.bmWidth;
dc.FillRect(rect, pBrush);
delete pBrush;
dc.Detach();
}
What the OnDrawItem does is; it draws a horizontal color bar with a color, prefixed by a check icon if that color is selected and the menu item being hovered over is highlighted by a box being drawn around it.
However, since I'm turning this application into a Multidoc application and I don't really feel that this logic should be in the CMainWnd (since none of the other documents will have this type of menu), but that it should be part of the CChildWnd (which inherits from CMDIChildWnd).
But when I move this logic to that class, when I run the application, I get following message in the console logger:
Warning: unknown WM_MEASUREITEM for menu item 0x0082.
And none of the custom menu behavior seems to work.
so, the question is; How can move the custom behavior of a menu into the frame class of an MDI document rather than having it located in the application main frame?
I figured out a work around. Not ideal but I can understand that this is a quirk in the framework, i.e. the menu seems to be part of the MainWnd so from a technical point of view, that is where the ON_WM_MEASUREITEM and ON_WM_DRAWITEM would be handled.
Anyhow, my work around. Basically capture the events in the MainWnd and then delegate the behaviour to the ChildWnd. The trick here (I guess) is to figure out what ChildWnd to delegate to since in an MDI application there can be any number of different ChildWnd's (each with their own Document and View types).
The work around:
void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
CMDIChildWnd* pActiveWnd = MDIGetActive();
if(pActiveWnd && pActiveWnd->IsWindowVisible())
{
if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;
CMyChildWnd->DoMeasureItem(nIDCtl, lpmis);
}
}
}
void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
CMDIChildWnd* pActiveWnd = MDIGetActive();
if(pActiveWnd && pActiveWnd->IsWindowVisible())
{
if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;
CMyChildWnd->DoDrawItem(nIDCtl, lpdis);
}
}
}
Pretty straight forward, in the context of the MainWnd, get a pointer to the active MDI ChildWnd, check if it is active, then check the type by using IsKindOf and RUNTIME_CLASS and if so, voila, delegate the behavior to the ChildWnd. To DoMeasureItem and the DoDrawItem are just public methods implemented on the ChildWnd (see question for details).

CMFCPopupMenu - RightClick Context menu shortcut keys disappear for some reason

I'm having a problem with the RMB Context menus, In my main frame I have a grid control with the RMB context menu event. The shortcut keys appear correctly. See Copy for Ctrl + C,Paste for Ctrl + V, etc...
Then I have a dialog with already a bunch of codes from other dev's. And this dialog somehow makes the RMB Context menu funky. The shortcut keys no longer appear. It may be caused by altering the PreTransalateMessage and some messages are not going through right or maybe some shell functions that may had a conflict. Because when you notice the window it's still using the windows aero basic theme while I'm using Windows 10 because of the pin icon beside the minimize.
This is the code used in the mainframe as well as in the dialog.
void MyDialog::OnContextMenu(CWnd* pWnd, CPoint ptMousePos)
{
CMenu *menuRightClick;
menuRightClick->LoadMenu(IDR_RIGHTCLICK);
CMenu *pPopupVitmMenu;
pPopupVitmMenu = menuRightClick->GetSubMenu(9);
ASSERT(pPopupVitmMenu);
if (pPopupVitmMenu)
{
CPoint point;
::GetCursorPos(&point);
CMFCPopupMenu* pPopupMenu = new CMFCPopupMenu;
CMFCPopupMenu* pPopup = CMFCPopupMenu::GetActiveMenu();
//close already poped up menus, if any.
if (pPopup != NULL)
pPopup->CloseMenu();
pPopupMenu->Create(this, point.x, point.y, pPopupVitmMenu->Detach(), FALSE, TRUE);
pPopupMenu->ShowWindow(SW_SHOW);
}
}
This maybe related or unrelated but the old context menu doesn't get dimissed when you RMB on another area. It only disappears when you click outside the application(Desktop,Taskbar).
I know the information is vague, but that's all I can provide.
Thanks in advance!
Use theApp.GetContextMenuManager() to get access to CContextMenuManager. Where theApp is the main CWinApp class. It should be calling InitContextMenuManager() during initialization.
CMenu menu;
menu.LoadMenu(IDR_MAINFRAME);
CMenu *popup = menu.GetSubMenu(0);
if(popup)
{
CContextMenuManager *manager = theApp.GetContextMenuManager();
if(manager)
//for CDialogEx:
manager->ShowPopupMenu(popup->Detach(), p.x, p.y, this, TRUE, TRUE, FALSE);
//for CDialog:
//manager->ShowPopupMenu(popup->Detach(), p.x, p.y, this, FALSE, TRUE, FALSE);
}
Note that the 5th parameter should be TRUE for CDialogEx, and FALSE for CDialog

PopupMenu from MenuBar when right click

Here is the piece of code that gives me a problem :
void CMainFrame::DisplayActionsPopupMenu()
{
// get "Actions" menu
wxMenuBar* pMenuBar = GetMenuBar();
ASSERT(pMenuBar != NULL);
int nIndex = pMenuBar->FindMenu("Actions");
ASSERT(nIndex != wxNOT_FOUND);
wxMenu *pMenuActions = pMenuBar->GetMenu(nIndex);
ASSERT(pMenuActions != NULL);
// display a popup menu for actions
PopupMenu(pMenuActions);
}
What I try to do here is to display a popupmenu when right clicking and I want it to be the same as the second menu in the menubar of my project.
It worked when I compiled with wxWidgets v2.8
Now I tried with v3.0 and here is the error:
../src/common/menucmn.cpp(715): assert "!IsAttached()" failed in SetInvokingWindow(): menus attached to menu bar can't have invoking window
What should I do to fix this?
I think a more robust solution than the existing answer consisting in detaching and attaching back the menu would be to just create a new menu instead, e.g. something like this:
std::unique_ptr<wxMenu> CreateActionsMenu() { ... }
// In your frame ctor or wherever you initialize your menu bar.
MyFrame::MyFrame() {
wxMenuBar* const mb = new wxMenuBar;
mb->Append(CreateActionsMenu().release(), "&Actions");
SetMenuBar(mb);
}
// In your event handler function showing the popup menu.
void MyFrame::OnShowPopup(wxCommandEvent&) {
auto menu = CreateActionsMenu();
PopupMenu(menu.get());
}
Creating a menu is relatively fast and there should be no problem doing it just before showing it (although, of course, you could also cache it for later if it's really huge or otherwise expensive to construct).
Finally I found that with the >3.0 wxWidgets version, you can't get elements from the wxMenuBar which is attached to your frame. So you have to temporarly unattach and reattach it.
Here is how you would so:
1 - Initialize the new wxMenu with the MenuBar. In my case:
wxMenuBar* pMenuBar = GetMenuBar();
ASSERT(pMenuBar != NULL);
cout<<pMenuBar->IsAttached()<<endl;
int nIndex = pMenuBar->FindMenu("Actions");
ASSERT(nIndex != wxNOT_FOUND);
wxMenu *pMenuActions = pMenuBar->GetMenu(nIndex);
2 - Check if it's attached:
if(pMenuActions->IsAttached()){
pMenuActions->Detach();
}
3 - When your done, reAttach the wxMenu to the wxMenuBar
pMenuActions->Attach(pMenuBar);

C++ CButton showing only bitmap icon

I'm using C++ on Visual Studio 2012 update 4, and I have a Dialog where I want to display a button showing a bitmap (.bmp file), without borders
I have extended CButton to add my tooltip, and so on.
Using the Resource View to open the Dialog .rc file, I set the button Property Bitmap to true. Then, from the Dialog OnInitDialog function, I used this code to set the bitmap, identified as IDB_HELP
myButton.SetBitmap((HBITMAP)LoadImage(AfxGetApp()->m_hInstance,
MAKEINTRESOURCE(IDB_HELP), IMAGE_BITMAP, 16, 16, LR_COLOR));
But it displays this and I don't want that half-border.
I tried making it Flat and Transparent in the Resource View, but it only gets uglier.
Then I tried to only draw the image by setting Owner Draw to true and then redefining DrawItem in my button class, but I can't quite figure that out either.
Any easy way to make an icon-only button?
You have to use owner draw button or custom draw. Below is a simple example, it uses icon instead of bitmap (it's easier to assign transparent background for it)
class CMyButton:public CButton
{
void OnPaint()
{
CPaintDC dc(this);
CRect rc = dc.m_ps.rcPaint;
dc.FillSolidRect(&rc, GetSysColor(COLOR_3DFACE));
BOOL offset = (BST_PUSHED & GetState()) ? 1 : 0;
int w = 24;
int h = 24;
HICON hicon = (HICON)LoadImage(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_ICON),
IMAGE_ICON, w, h, LR_DEFAULTCOLOR);
DrawIconEx(dc, offset, offset, hicon, w, h, 0, 0, DI_NORMAL);
DestroyIcon(hicon);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyButton, CButton)
ON_WM_PAINT()
END_MESSAGE_MAP()
Usage:
BOOL CMyDialog::OnInitDialog()
{
BOOL res = CDialogEx::OnInitDialog();
static CMyButton bn;
bn.SubclassDlgItem(IDC_BUTTON1, this);
return res;
}
You do NOT need to do your own icon painting algorithm if you use a CMFCButton and you are a comfortable using an ICO file instead of a BMP. Although you can directly say in your resources file a button is of this type, I do not recommend it, because it adds an unmaintainable hexadecimal piece of text on the rc file. And if you use several rc files, one for each language, it's really devilish!
So lets go. In your form class, declare a member
CMFCButton m_button1;
The DoDataExchange should look like:
void MyDialog::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_BUTTON1, m_button1);
// ...
}
Then the OnInitDialog should be something like:
BOOL CMyDialog::OnInitDialog()
{
if(!__super::OnInitDialog())
return FALSE;
m_button1.m_nFlatStyle= CMFCButton::BUTTONSTYLE_NOBORDERS;
m_button1.SetIcon(IDI_HELP);
return TRUE;
}
Use CMFCbutton and set the border style to BUTTONSTYLE_NOBORDERS;
Use a .ico instead of png for pictures.
Also points to note:
Load the library or exe which has the icon.
Pass the dll/exe loaded handle to loadicon.
Use MFC button handle to set the icon with property set as Noborder.
Example code:
m_HResdll = LoadLibrary("C:\\Repos\\iFIX\\SCADABin\\en\\UAAClientConfigurationRes.dll");
//m_hTrustIcon = LoadIcon(m_HResdll, MAKEINTRESOURCE(IDI_ICON1));
m_hTrustIcon = (HICON)LoadImage(m_HResdll, MAKEINTRESOURCE(IDI_ICON2),1,18,22, LR_DEFAULTCOLOR);
unsigned int err = GetLastError();
m_btnTrustIcon.SetIcon(m_hTrustIcon);
this->m_btnTrustIcon.EnableWindow(true);

CMFCCaptionMenuButton alternative?

I need to create a caption bar button for a CDockablePane which will call up a menu with various options. I tried to use CMFCCaptionMenuButton and the button and menu show up but the message map methods for the menu ids don't fire. The MFC documentation states that CMFCCaptionMenuButton is meant for internal infrastructure and not really for your code.
So assuming that is what my problem is should I be using a CMFCCaptionBarButton and then making a separate popup menu? Has anyone made a similar caption bar based menu in MFC before?
Here's some slimmed down code snippets in case I just made a stupid mistake in hooking up the events:
BEGIN_MESSAGE_MAP(CDockPane, CDockablePane)
ON_COMMAND(ID_MORPH_BROWSER, OnMorphBrowser)
END_MESSAGE_MAP()
void CDockPane::OnPressButtons(UINT nHit)
{
// only for custom button handling don't call base
// close, maximize, and pin will be handled by default
switch (nHit)
{
case ID_MORPHTEST:
{
CMorphMenuButton* pButton = dynamic_cast<CMorphMenuButton*>(m_arrButtons.GetAt(m_morphIndex));
pButton->ShowMenu(this);
break;
}
}
}
void CDockPane::SetCaptionButtons()
{
CDockablePane::SetCaptionButtons(); // for close, pin etc
m_morphIndex = m_arrButtons.Add(new CMorphMenuButton(ID_MORPHTEST));
}
void CDockPane::OnMorphBrowser()
{
// do stuff on menu item click
}
Edit: Removed previous code no longer in use
Now that the sound of crickets chirping has dwindled in the background I guess I'll post the workaround I currently have in place:
Instead of inheriting and extending CMFCCaptionMenuButton I build my class by extending CMFCCaptionButton. I then create a menu and provide a ShowMenu method to be explicitly called when handling the custom button events as well as overriding GetIconID to return a particular system icon for the button for each menu added to the caption bar ending up with something like this for the example outlined in the question:
#pragma once
// CMorphMenuButton command target
class CMorphMenuButton : public CMFCCaptionButton
{
public:
CMorphMenuButton(UINT nHit);
virtual ~CMorphMenuButton();
virtual CMenuImages::IMAGES_IDS GetIconID (BOOL bHorz, BOOL bMaximized) const;
void ShowMenu(CWnd* pWnd);
private:
CMenu m_dockMenu;
CMenu* m_subMenu;
};
// MorphMenuButton.cpp : implementation file
//
#include "stdafx.h"
#include "MorphMenuButton.h"
// CMorphMenuButton
CMorphMenuButton::CMorphMenuButton(UINT nHit)
: CMFCCaptionButton(nHit)
{
SetMiniFrameButton(); // already defaulted?
m_dockMenu.LoadMenu(IDR_DOCKPANE); // resource ID for dock pane menus
}
CMorphMenuButton::~CMorphMenuButton()
{
m_dockMenu.DestroyMenu();
}
CMenuImages::IMAGES_IDS CMorphMenuButton::GetIconID(BOOL bHorz, BOOL bMaximized) const
{
return CMenuImages::IdArrowForward;
}
void CMorphMenuButton::ShowMenu(CWnd* pWnd)
{
CRect windowRect, buttonRect;
pWnd->GetWindowRect(&windowRect);
buttonRect = GetRect();
CPoint menuPos(windowRect.left + buttonRect.right, windowRect.top + buttonRect.bottom);
m_subMenu = m_dockMenu.GetSubMenu(0);
if (!m_subMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, menuPos.x, menuPos.y, pWnd))
{
DWORD id = GetLastError();
wchar_t errMsg[256];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, id, 0, errMsg, sizeof(errMsg), 0);
MessageBox(0, errMsg, L"Error", MB_OK);
}
}
The setting of caption bar buttons and handling of click events for both buttons and menus are the same as defined in the question and this works.