3

I'm working on an MFC MDI application. I would like to selectively enable or disable some right-click context menu commands, but the menu commands remain active after I implemented functionality using ON_UPDATE_COMMAND_UI to disable certain commands.

It appears that the ON_UPDATE_COMMAND_UI message is not being triggered or handled at the appropriate time.

The goal is to implement a CTreeCtrl context-menu which depends on the selected tree element.

My application structure

The main CWinAppEx launches a CMDIFrameWndEx. The CMDIFrameWndEx object contains normal MDI child-frames, but it also contains a CDockablePane which itself contains a CTreeCtrl, intended to be used similiarly to the Project or Solution tree-view in Visual Studio.

enter image description here

I've implemented ON_UPDATE_COMMAND_UI message-handlers in my class inheriting from CTreeCtrl. These handlers are being called when I click the menu-items themselves, but this is too late; they should have been called before the menu opens.

I suspect that this is related to command (or message) routing; Googling around indicates that some control or window-classes do not receive ON_COMMAND_UPDATE_UI messages because they're handled at the CFrameWnd level. However, the work-arounds or solutions presented in those discussions are not clearly articulated. I want to adhere to common MFC/MDI idioms, so I'm hoping for a somewhat-comprehensive, beginner-level explanation of this issue.

Are CDockablePane windows (or CTreeCtrl controls) not intended to interact with ON_COMMAND_UPDATE_UI? Why is this? Am I missing something else?

My instinct is that this should be possible without wiring up a bunch of events myself, because the MFC framework is supposed to deliver messages to any classes which may handle them. I am starting to think there is some subtlety to this behavior that I'm missing.

Code

From my CTreeCtrl-inheriting class (CLCPViewTree):

BEGIN_MESSAGE_MAP(CLCPViewTree, CTreeCtrl)
    ON_NOTIFY_REFLECT(NM_RCLICK, OnRClick)
    ON_COMMAND(ID_LCPGATEWAYCONTEXTMENU_SAVE_SESSION, SaveSession)
    ON_COMMAND(ID_LCPGATEWAYCONTEXTMENU_LOAD_SESSION, LoadSession)
    ON_COMMAND(ID_LCPGATEWAYCONTEXTMENU_SAVE, SaveDocument)
    ON_COMMAND(ID_LCPGATEWAYCONTEXTMENU_LOAD, LoadDocument)
    ON_UPDATE_COMMAND_UI(ID_LCPGATEWAYCONTEXTMENU_LOAD, OnUpdateMenuLoadSingle)
    ON_UPDATE_COMMAND_UI(ID_LCPGATEWAYCONTEXTMENU_SAVE, OnUpdateMenuLoadSingle)
    ON_UPDATE_COMMAND_UI(ID_LCPGATEWAYCONTEXTMENU_LOAD_SESSION, OnUpdateMenuLoadSession)
    ON_UPDATE_COMMAND_UI(ID_LCPGATEWAYCONTEXTMENU_SAVE_SESSION, OnUpdateMenuSaveSession)
    ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()

afx_msg void CLCPViewTree::OnUpdateMenuLoadSingle(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(m_bShowSingleInstanceMenu);
}

afx_msg void CLCPViewTree::OnUpdateMenuSaveSingle(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(m_bShowSingleInstanceMenu);
}

afx_msg void CLCPViewTree::OnUpdateMenuLoadSession(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(m_bShowSessionMenu);
}

afx_msg void CLCPViewTree::OnUpdateMenuSaveSession(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(m_bShowSessionMenu);
}

void CLCPViewTree::OnRClick(NMHDR* pNMHDR, LRESULT* pResult) 
{
    TRACE0("CLCPViewTree::OnRClick()\r\n");

    HTREEITEM hItem = GetSelectedItem();

    if(!hItem)
    {
        return;
    }

    CString text = GetItemText(hItem);
    TRACE0(text);

    //To get your element:
     SelectorReference* ref = (SelectorReference *) (GetItemData(hItem));
     
    if(ref == nullptr)
    {
        m_bShowSessionMenu = false;
        m_bShowSingleInstanceMenu = false;
    }
    else if(ref->is_program)
    {
        m_bShowSessionMenu = false;
        m_bShowSingleInstanceMenu = true;
        
        // Send WM_CONTEXTMENU to self
        CString path = CString(ref->p_pd->ProgramPath) + "sim";
        TRACE0("Controller path:\r\n");
        TRACE0(path + "\r\n");
        SelectedControllerPath = path;
        SelectedLCPGatewayView = ref->view;
        SendMessage(WM_CONTEXTMENU, (WPARAM) m_hWnd, GetMessagePos());
    }
    else
    {
        m_bShowSessionMenu = false;
        m_bShowSingleInstanceMenu = false;
    }
    
    // Mark message as handled and suppress default handling
    *pResult = 1;
}

void CLCPViewTree::OnContextMenu(CWnd* pWnd, CPoint ptMousePos) 
{
    // if Shift-F10
    if (ptMousePos.x == -1 && ptMousePos.y == -1)
        ptMousePos = (CPoint) GetMessagePos();

    ScreenToClient(&ptMousePos);
    
    CMenu menu;
    CMenu* pPopup;
    
    // the font popup is stored in a resource
    menu.LoadMenu(IDR_PROGRAM_MENU);
    pPopup = menu.GetSubMenu(0);
    ClientToScreen(&ptMousePos);
    pPopup->TrackPopupMenu( TPM_LEFTALIGN, ptMousePos.x, ptMousePos.y, this );
}

New code

I have since added overrides for OnCmdMsg to my CMDIFrameWndEx-inherited class (CMainFrame), and to my CDockablePane-inherited class (LCPSelector). The intention is to pass command-messages all the way down to the CTreeCtrl object in case they may be handled.

I added this code based on the discussion of command-routing here:
https://learn.microsoft.com/en-us/cpp/mfc/command-routing?view=vs-2019

Still getting the same results, though. Maybe this is the wrong direction, or maybe I'm missing something else too.

For reference: CMainFrame inherits from CMDIFrameWndEx, and LCPSelector inherits from CDockablePane.

BOOL CMainFrame::OnCmdMsg(UINT id,int code , void *pExtra,AFX_CMDHANDLERINFO* pHandler)
{
   //route cmd first to registered dockable pane
    if(m_wndSelector.OnCmdMsg(id,code,pExtra,pHandler))
    {
        return TRUE;
    }
  
  return CMDIFrameWndEx::OnCmdMsg(id,code,pExtra,pHandler);
}

BOOL LCPSelector::OnCmdMsg(UINT id,int code , void *pExtra,AFX_CMDHANDLERINFO* pHandler)
{
   //route cmd first to registered dockable pane
    if(m_ctrlLCPViewTree.OnCmdMsg(id,code,pExtra,pHandler))
    {
        return TRUE;
    }
  
  return CDockablePane::OnCmdMsg(id,code,pExtra,pHandler);
}
3
  • 1
    I think you want to display a popup menu, and you want to enable/disable the menu items. Put pPopup->EnableMenuItem(menu_item_id, MF_ENABLED/ MF_GRAYED) before calling pPopup->TrackPopupMenu Commented Oct 7, 2019 at 22:19
  • @BarmakShemirani Thank you, your suggestion does work. However, several other blog posts and discussions recommended the approach using ON_UPDATE_COMMAND_UI. I'm not just trying to "get this working", I also want to understand: why is the ON_UPDATE_COMMAND_UI method failing for me?
    – afarley
    Commented Oct 7, 2019 at 22:41
  • I believe ON_UPDATE_COMMAND_UI is called from an idle loop. Commented Oct 8, 2019 at 0:57

1 Answer 1

3

When the menu is opened, WM_INITMENUPOPUP message is generated, it calls OnInitMenuPopup.

OnInitMenuPopup updates the menu based on ON_UPDATE_COMMAND_UI

If you call popup->TrackPopupMenu(TPM_LEFTALIGN, x, y, this) in your CTreeCtrl class, then you have to handle OnInitMenuPopup yourself. Otherwise ON_UPDATE_COMMAND_UI is ignored.

In many cases it would be easier to use AfxGetMainWnd() as the window handle for menu's parent. This should call CFrameWnd::OnInitMenuPopup which will update the menu items based on ON_UPDATE_COMMAND_UI commands.

Make sure to put ON_COMMAND and ON_UPDATE_COMMAND_UI in your CDockablePane or CMDIFrameWndEx class. Example:

BEGIN_MESSAGE_MAP(CMyDockablePane, CDockablePane)
    ON_COMMAND(ID_X, OnFoo)
    ON_UPDATE_COMMAND_UI(ID_X, OnFooUpdate)
    ...
END_MESSAGE_MAP()

...
//use AfxGetMainWnd() instead of this handle
popup->TrackPopupMenu(TPM_LEFTALIGN, pt.x, pt.y, AfxGetMainWnd());


Alternatively, you can call this from CTreeCtrl class, but you have to override OnInitMenuPopup

BEGIN_MESSAGE_MAP(CMyTreeCtrl, CTreeCtrl)
    ON_WM_INITMENUPOPUP()
    ON_COMMAND(ID_X, OnFoo)
    ON_UPDATE_COMMAND_UI(ID_X, OnFooUpdate)
    ...
END_MESSAGE_MAP()

void CMyTreeCtrl::OnInitMenuPopup(CMenu* popup, UINT nIndex, BOOL bSysMenu)
{
    if(popup && !bSysMenu)
    {
        CCmdUI state;
        state.m_pMenu = popup;
        state.m_nIndexMax = popup->GetMenuItemCount();
        for(UINT i = 0; i < state.m_nIndexMax; i++)
        {
            state.m_nIndex = i;
            state.m_nID = popup->GetMenuItemID(i);
            state.DoUpdate(this, FALSE);
        }
    }
}
...
//call from CMyTreeCtrl with this handle
popup->TrackPopupMenu(TPM_LEFTALIGN, pt.x, pt.y, this);

See also
When Update Handlers Are Called

You cannot change the state of a menu item...

Not the answer you're looking for? Browse other questions tagged or ask your own question.