如何在CTreeCtrl/CWnd中接收ON_UPDATE_COMMAND_UI?
How to receive ON_UPDATE_COMMAND_UI in CTreeCtrl/CWnd?
我正在开发 MFC MDI 应用程序。我想有选择地启用或禁用某些右键单击上下文菜单命令,但在我使用 ON_UPDATE_COMMAND_UI 实现功能以禁用某些命令后,菜单命令仍然处于活动状态。
ON_UPDATE_COMMAND_UI 消息似乎未在适当的时间触发或处理。
目标是实现一个依赖于所选树元素的 CTreeCtrl 上下文菜单。
我的申请结构
主 CWinAppEx 启动 CMDIFrameWndEx。 CMDIFrameWndEx 对象包含正常的 MDI 子框架,但它还包含一个 CDockablePane,它本身包含一个 CTreeCtrl,旨在与 Visual Studio 中的项目或解决方案树视图类似地使用。
我在继承自 CTreeCtrl 的 class 中实现了 ON_UPDATE_COMMAND_UI 消息处理程序。 当我单击菜单项本身时会调用这些处理程序,但为时已晚;他们应该在菜单打开之前被调用。
我怀疑这与命令(或消息)路由有关;谷歌搜索表明某些控件或 window-classes 没有收到 ON_COMMAND_UPDATE_UI 消息,因为它们是在 CFrameWnd 级别处理的。但是,这些讨论中提出的变通办法或解决方案并未明确阐述。我想遵循常见的 MFC/MDI 习语,所以我希望对这个问题有一个比较全面的初学者级别的解释。
CDockablePane windows(或 CTreeCtrl 控件)是否不打算与 ON_COMMAND_UPDATE_UI 交互?为什么是这样?我还漏掉了什么吗?
我的直觉是,这应该是可能的,而无需我自己连接一堆事件,因为 MFC 框架应该将消息传递给可以处理它们的任何 classes。我开始认为我遗漏了这种行为的一些微妙之处。
代码
来自我的 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 );
}
新密码
我已将 OnCmdMsg 的覆盖添加到我的 CMDIFrameWndEx 继承 class (CMainFrame) 和我的 CDockablePane 继承 class (LCPSelector)。目的是将命令消息一直向下传递到 CTreeCtrl 对象,以防它们被处理。
我根据此处对命令路由的讨论添加了这段代码:
https://docs.microsoft.com/en-us/cpp/mfc/command-routing?view=vs-2019
不过,仍然得到相同的结果。也许这是错误的方向,或者我也错过了其他东西。
供参考:CMainFrame继承自CMDIFrameWndEx,LCPSelector继承自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);
}
打开菜单时,生成WM_INITMENUPOPUP
消息,调用OnInitMenuPopup
。
OnInitMenuPopup
根据 ON_UPDATE_COMMAND_UI
更新菜单
如果您在 CTreeCtrl
class 中调用 popup->TrackPopupMenu(TPM_LEFTALIGN, x, y, this)
,那么您必须自己处理 OnInitMenuPopup
。否则 ON_UPDATE_COMMAND_UI
将被忽略。
在许多情况下,使用 AfxGetMainWnd()
作为菜单父级的 window 句柄会更容易。这应该调用 CFrameWnd::OnInitMenuPopup
,它将根据 ON_UPDATE_COMMAND_UI
命令更新菜单项。
确保在 CDockablePane
或 CMDIFrameWndEx
class 中输入 ON_COMMAND
和 ON_UPDATE_COMMAND_UI
。示例:
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());
或者,您可以从 CTreeCtrl
class 调用它,但您必须覆盖 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);
我正在开发 MFC MDI 应用程序。我想有选择地启用或禁用某些右键单击上下文菜单命令,但在我使用 ON_UPDATE_COMMAND_UI 实现功能以禁用某些命令后,菜单命令仍然处于活动状态。
ON_UPDATE_COMMAND_UI 消息似乎未在适当的时间触发或处理。
目标是实现一个依赖于所选树元素的 CTreeCtrl 上下文菜单。
我的申请结构
主 CWinAppEx 启动 CMDIFrameWndEx。 CMDIFrameWndEx 对象包含正常的 MDI 子框架,但它还包含一个 CDockablePane,它本身包含一个 CTreeCtrl,旨在与 Visual Studio 中的项目或解决方案树视图类似地使用。
我在继承自 CTreeCtrl 的 class 中实现了 ON_UPDATE_COMMAND_UI 消息处理程序。 当我单击菜单项本身时会调用这些处理程序,但为时已晚;他们应该在菜单打开之前被调用。
我怀疑这与命令(或消息)路由有关;谷歌搜索表明某些控件或 window-classes 没有收到 ON_COMMAND_UPDATE_UI 消息,因为它们是在 CFrameWnd 级别处理的。但是,这些讨论中提出的变通办法或解决方案并未明确阐述。我想遵循常见的 MFC/MDI 习语,所以我希望对这个问题有一个比较全面的初学者级别的解释。
CDockablePane windows(或 CTreeCtrl 控件)是否不打算与 ON_COMMAND_UPDATE_UI 交互?为什么是这样?我还漏掉了什么吗?
我的直觉是,这应该是可能的,而无需我自己连接一堆事件,因为 MFC 框架应该将消息传递给可以处理它们的任何 classes。我开始认为我遗漏了这种行为的一些微妙之处。
代码
来自我的 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 );
}
新密码
我已将 OnCmdMsg 的覆盖添加到我的 CMDIFrameWndEx 继承 class (CMainFrame) 和我的 CDockablePane 继承 class (LCPSelector)。目的是将命令消息一直向下传递到 CTreeCtrl 对象,以防它们被处理。
我根据此处对命令路由的讨论添加了这段代码:
https://docs.microsoft.com/en-us/cpp/mfc/command-routing?view=vs-2019
不过,仍然得到相同的结果。也许这是错误的方向,或者我也错过了其他东西。
供参考:CMainFrame继承自CMDIFrameWndEx,LCPSelector继承自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);
}
打开菜单时,生成WM_INITMENUPOPUP
消息,调用OnInitMenuPopup
。
OnInitMenuPopup
根据 ON_UPDATE_COMMAND_UI
如果您在 CTreeCtrl
class 中调用 popup->TrackPopupMenu(TPM_LEFTALIGN, x, y, this)
,那么您必须自己处理 OnInitMenuPopup
。否则 ON_UPDATE_COMMAND_UI
将被忽略。
在许多情况下,使用 AfxGetMainWnd()
作为菜单父级的 window 句柄会更容易。这应该调用 CFrameWnd::OnInitMenuPopup
,它将根据 ON_UPDATE_COMMAND_UI
命令更新菜单项。
确保在 CDockablePane
或 CMDIFrameWndEx
class 中输入 ON_COMMAND
和 ON_UPDATE_COMMAND_UI
。示例:
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());
或者,您可以从
CTreeCtrl
class 调用它,但您必须覆盖 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);