QA: How to enable update command UI mechanism in dialogs?
Alexander Shargin (rudankort@softspb.com), June 19, 2003.
Question
I am writing an MFC-based application. I added a menu and/or a toolbar to one of
my dialogs. I also wrote ON_UPDATE_COMMAND_UI handlers for it. But they are
never called. How can I fix this?
Answer
Update command UI mechanism is a convenient way to update state of toolbars
and menus in MFC. However, this mechanism is implemented in CFrameWnd class.
This means that it works in the main window based on CFrameWnd but doesn't
work in other windows. In order to use it in a dialog or any other window,
you basically have to implement it from scratch using the code from CFrameWnd
as an example. In this article I will show you how to do this.
Updating popup menus
In Windows, popup menus are initialized in response to WM_INITMENUPOPUP
message. CFrameWnd has a handler for this message called OnInitMenuPopup.
This is where MFC calls your ON_UPDATE_COMMAND_UI handlers for menu items.
So you should add a OnInitMenuPopup handler to your dialog and copy all the
code from CFrameWnd handler.
Note: by default ClassWizard doesn't allow you to add WM_INITMENUPOPUP
handelr to a dialog class. To change this behaviour, open ClassWizard,
choose your dialog class from Class name combo box, go to
Class Info tab and choose Message filter: Window.
Here is how OnInitMenuPopup handler can look (it is just a slightly
modified version of CFrameWnd::OnInitMenuPopup).
void CDialogUpdateCmdUIDlg::OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu)
{
if (bSysMenu)
return; // don't support system menu
ASSERT(pMenu != NULL);
// check the enabled state of various menu items
CCmdUI state;
state.m_pMenu = pMenu;
ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pParentMenu == NULL);
// determine if menu is popup in top-level menu and set m_pOther to
// it if so (m_pParentMenu == NULL indicates that it is secondary popup)
HMENU hParentMenu;
if (AfxGetThreadState()->m_hTrackingMenu == pMenu->m_hMenu)
state.m_pParentMenu = pMenu; // parent == child for tracking popup
else if ((hParentMenu = ::WCE_FCTN(GetMenu)(m_hWnd)) != NULL)
{
CWnd* pParent = GetTopLevelParent();
// child windows don't have menus -- need to go to the top!
if (pParent != NULL &&
(hParentMenu = ::WCE_FCTN(GetMenu)(pParent->m_hWnd)) != NULL)
{
int nIndexMax = ::WCE_FCTN(GetMenuItemCount)(hParentMenu);
for (int nIndex = 0; nIndex < nIndexMax; nIndex++)
{
if (::GetSubMenu(hParentMenu, nIndex) == pMenu->m_hMenu)
{
// when popup is found, m_pParentMenu is containing menu
state.m_pParentMenu = CMenu::FromHandle(hParentMenu);
break;
}
}
}
}
state.m_nIndexMax = pMenu->GetMenuItemCount();
for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax;
state.m_nIndex++)
{
state.m_nID = pMenu->GetMenuItemID(state.m_nIndex);
if (state.m_nID == 0)
continue; // menu separator or invalid cmd - ignore it
ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pMenu != NULL);
if (state.m_nID == (UINT)-1)
{
// possibly a popup menu, route to first item of that popup
state.m_pSubMenu = pMenu->GetSubMenu(state.m_nIndex);
if (state.m_pSubMenu == NULL ||
(state.m_nID = state.m_pSubMenu->GetMenuItemID(0)) == 0 ||
state.m_nID == (UINT)-1)
{
continue; // first item of popup can't be routed to
}
state.DoUpdate(this, FALSE); // popups are never auto disabled
}
else
{
// normal menu item
// Auto enable/disable if frame window has 'm_bAutoMenuEnable'
// set and command is _not_ a system command.
state.m_pSubMenu = NULL;
state.DoUpdate(this, TRUE);
}
// adjust for menu deletions and additions
UINT nCount = pMenu->GetMenuItemCount();
if (nCount < state.m_nIndexMax)
{
state.m_nIndex -= (state.m_nIndexMax - nCount);
while (state.m_nIndex < nCount &&
pMenu->GetMenuItemID(state.m_nIndex) == state.m_nID)
{
state.m_nIndex++;
}
}
state.m_nIndexMax = nCount;
}
}
Updating toolbars
Each command bar is responsible for updating its controls in
response to WM_IDLEUPDATECMDUI message. MFC sends this message
to the main frame and all of its descendants in the idle time
(this is done in CWinApp::OnIdle handler). Then this message is
handled in CControlBar class which is the base calss for all bar
classes in MFC. Actual updating is performed in OnUpdateCmdUI method
which is virtual and is overriden in every CControlBar-derived class.
So to update your dialog's toolbar, all you need is to call its
OnUpdateCmdUI method:
m_pWndEmptyCB->OnUpdateCmdUI((CFrameWnd *)this, TRUE);
Note: as you can see, this pointer is cast to CFrameWnd*. This is safe
since OnUpdateCmdUI doesn't use any CFrameWnd-specific features through this pointer.
The problem is, where to call this method. In desktop version of MFC all
modal windows (including dialogs) can use built-in idle mechanism: when
there are no more messages in the message queue, CWnd::RunModalLoop sends a
special WM_KICKIDLE message (#defined in afxpriv.h) to the modal window. You
can handle this message and perform any idle-time work in your handler.
However, on CE this mechanism is removed from modal loop with the following
comment:
// WinCE: no idle processing should be done by Windows CE apps
So you need to reinvent the wheel once again and implement idle mechanism yourself.
When it comes to modifying message loop behaviour, your hook to the message
loop is CWnd::PreTranslateMessage. In this method you can check if no more
messages are waiting in the queue (use PeekMessage for this). If the queue is
empty, it is time to perform updating of your toolbar.
Here is what your PreTranslateMessage override may look like.
BOOL CDialogUpdateCmdUIDlg::PreTranslateMessage(MSG* pMsg)
{
static BOOL bDoIdle = TRUE;
MSG msg;
if(!::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE) && bDoIdle)
{
m_pWndEmptyCB->OnUpdateCmdUI((CFrameWnd *)this, TRUE);
bDoIdle = FALSE;
}
else
{
if(AfxGetApp()->IsIdleMessage(pMsg) && pMsg->message != 0x3FC)
{
bDoIdle = TRUE;
}
}
return CDialog::PreTranslateMessage(pMsg);
}
I use CWinThread::IsIdleMessage to determine if a message should trigger
idle processing. This function doesn't work correctly on Pocket PC 2002
devices (I described this problem here). To
fix this, additional check if performed in the if statement.
DialogUpdateCmdUI demo project illustrates how these techniques are
used in a real application.
Sample
You can download a sample application DialogUpdateCmdUI.zip (19 Kb).
Related resources:
-
http://www.pocketpcdn.com/sections/ui.html
Section: User Interface
-
http://www.pocketpcdn.com/articles/cmdbar_menu.html
QA: How do I get the command bar menu handle?
-
http://www.pocketpcdn.com/articles/second_commandbar.html
QA: How can I implement the second toolbar like the one in Pocket Word?
-
http://www.pocketpcdn.com/articles/dropdown_button.html
QA: How to add dropdown buttons in the CCeCommandBar?
-
http://www.pocketpcdn.com/articles/dialogbar.html
QA: How can I use a command bar in a dialog?
-
http://www.pocketpcdn.com/articles/remove_new_button.html
QA: How to remove New button from command bar?
-
http://www.pocketpcdn.com/articles/updatecommandui.html
QA: Why ON_UPDATE_COMMAND_UI handlers get called again and again?