作者:劉樹偉
MDI結構中,當沒有打開任何文檔時,主框架有個默認的菜單。這個菜單,提供了基本的文件打開、關閉程序、幫助等功能,對應CFrameWnd::m_hMenuDefault,由CMDIFrameWnd::LoadFrame第一個參數決定,一般是由ID爲IDR_MAINFRAME的菜單資源創建的。菜單的創建過程,在CFrameWnd::Create中,由CMDIFrameWnd::LoadFrame調用。請注意:CMDIFrameWnd是派生自CFrameWnd的,CMDIFrameWnd繼承了CFrameWnd::m_hMenuDefault。
下面是菜單m_hMenuDefault的創建代碼:
BOOL CFrameWnd::Create(LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
LPCTSTR lpszMenuName,
DWORD dwExStyle,
CCreateContext* pContext)
{
HMENU hMenu = NULL;
if (lpszMenuName != NULL)
{
// load in a menu that will get destroyed when window gets destroyed
HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU);
if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL) // 創建菜單
{
TRACE0("Warning: failed to load menu for CFrameWnd.\n");
PostNcDestroy(); // perhaps delete the C++ object
return FALSE;
}
}
m_strTitle = lpszWindowName; // save title for later
if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext)) // 把菜單設置到新創建的窗口上。
{
TRACE0("Warning: failed to create CFrameWnd.\n");
if (hMenu != NULL)
DestroyMenu(hMenu);
return FALSE;
}
return TRUE;
}
CFrameWnd::Create由CFrameWnd::LoadFrame調用
BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)
{
// only do this once
ASSERT_VALID_IDR(nIDResource);
ASSERT(m_nIDHelp == 0 || m_nIDHelp == nIDResource);
m_nIDHelp = nIDResource; // ID for help context (+HID_BASE_RESOURCE)
CString strFullString;
if (strFullString.LoadString(nIDResource))
AfxExtractSubString(m_strTitle, strFullString, 0); // first sub-string
VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
// attempt to create the window
LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource);
LPCTSTR lpszTitle = m_strTitle;
if (!Create(lpszClass, lpszTitle, dwDefaultStyle, rectDefault,
pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext))
{
return FALSE; // will self destruct on failure normally
}
// save the default menu handle
ASSERT(m_hWnd != NULL);
m_hMenuDefault = ::GetMenu(m_hWnd); // 通過得到新創建窗口的菜單,爲m_hMenuDefault賦值。
// load accelerator resource
LoadAccelTable(MAKEINTRESOURCE(nIDResource));
if (pContext == NULL) // send initial update
SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE);
return TRUE;
}
////////////////////////////////////////////////////////////
當打開了一種文檔後,菜單欄中的菜單,會切換到與此文檔相對應的菜單,每種文檔類型,對應不同的菜單。這些文檔菜單,對應CMDIChildWnd::m_hMenuShared,CMDIChildWnd也是派生自CFrameWnd,當然也繼承了CFrameWnd::m_hMenuDefault,但CMDIChildWnd中的m_hMenuDefault,是無用的。
CMDIChildWnd::m_hMenuShared是在CMDIChildWnd::LoadFrame中被賦值的:
BOOL CMDIChildWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)
{
// only do this once
ASSERT_VALID_IDR(nIDResource);
ASSERT(m_nIDHelp == 0 || m_nIDHelp == nIDResource);
ASSERT(m_hMenuShared == NULL); // only do once
m_nIDHelp = nIDResource; // ID for help context (+HID_BASE_RESOURCE)
// parent must be MDI Frame (or NULL for default)
ASSERT(pParentWnd == NULL || pParentWnd->IsKindOf(RUNTIME_CLASS(CMDIFrameWnd)));
// will be a child of MDIClient
ASSERT(!(dwDefaultStyle & WS_POPUP));
dwDefaultStyle |= WS_CHILD;
// if available - get MDI child menus from doc template
ASSERT(m_hMenuShared == NULL); // only do once
CMultiDocTemplate* pTemplate;
if (pContext != NULL &&
(pTemplate = (CMultiDocTemplate*)pContext->m_pNewDocTemplate) != NULL)
{
ASSERT_KINDOF(CMultiDocTemplate, pTemplate);
// get shared menu from doc template
m_hMenuShared = pTemplate->m_hMenuShared; // 執行到這裏賦值
m_hAccelTable = pTemplate->m_hAccelTable;
}
else
{
TRACE0("Warning: no shared menu/acceltable for MDI Child window.\n");
// if this happens, programmer must load these manually
}
CString strFullString, strTitle;
if (strFullString.LoadString(nIDResource))
AfxExtractSubString(strTitle, strFullString, 0); // first sub-string
ASSERT(m_hWnd == NULL);
if (!Create(GetIconWndClass(dwDefaultStyle, nIDResource),
strTitle, dwDefaultStyle, rectDefault,
(CMDIFrameWnd*)pParentWnd, pContext))
{
return FALSE; // will self destruct on failure normally
}
// it worked !
return TRUE;
}
但CMDIChildWnd::m_hMenuShared不是自己通過LoadMenu之類初始化的,而是由CMultiDocTemplate::m_hMenuShared初始化的。CMultiDocTemplate::m_hMenuShared在CMultiDocTemplate::LoadTemplate()中,完成賦值:
void CMultiDocTemplate::LoadTemplate()
{
CDocTemplate::LoadTemplate();
if (m_nIDResource != 0 && m_hMenuShared == NULL)
{
HINSTANCE hInst = AfxFindResourceHandle(
MAKEINTRESOURCE(m_nIDResource), RT_MENU);
m_hMenuShared = ::LoadMenu(hInst, MAKEINTRESOURCE(m_nIDResource));
m_hAccelTable =
::LoadAccelerators(hInst, MAKEINTRESOURCE(m_nIDResource));
}
#ifdef _DEBUG
// warnings about missing components (don't bother with accelerators)
if (m_hMenuShared == NULL)
TRACE1("Warning: no shared menu for document template #%d.\n",
m_nIDResource);
#endif //_DEBUG
}
而m_nIDResource是在CMultiDocTemplate的構造函數中初始化,MDI App類的的InitInstance中,常見下面的代碼:
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_MFCMDITYPE, // 這個值,就用來初始化CMultiDocTemplate::m_nIDResource,最終用來生成文檔菜單。
RUNTIME_CLASS(CMFCMDIDoc),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CMFCMDIView));
////////////////////////////////////////////////////////////////////////////////
不管是切換到文檔對應的菜單,還是切換到主框架菜單,都是在CMDIChildWnd::OnUpdateFrameMenu中進行的:
void CMDIChildWnd::OnUpdateFrameMenu(BOOL bActivate, CWnd* pActivateWnd,
HMENU hMenuAlt)
{
CMDIFrameWnd* pFrame = GetMDIFrame(); // 得到主框架指針,通常爲CMainFrame
if (hMenuAlt == NULL && bActivate)
{
// attempt to get default menu from document
CDocument* pDoc = GetActiveDocument();
if (pDoc != NULL)
hMenuAlt = pDoc->GetDefaultMenu();
}
// use default menu stored in frame if none from document
if (hMenuAlt == NULL)
hMenuAlt = m_hMenuShared;
if (hMenuAlt != NULL && bActivate) // 切換到文檔菜單
{
ASSERT(pActivateWnd == this);
// activating child, set parent menu
::SendMessage(pFrame->m_hWndMDIClient, WM_MDISETMENU,
(WPARAM)hMenuAlt, (LPARAM)pFrame->GetWindowMenuPopup(hMenuAlt));
}
else if (hMenuAlt != NULL && !bActivate && pActivateWnd == NULL) // 切換到主框架菜單
{
// destroying last child
HMENU hMenuLast = NULL;
::SendMessage(pFrame->m_hWndMDIClient, WM_MDISETMENU,
(WPARAM)pFrame->m_hMenuDefault, (LPARAM)hMenuLast);
}
else
{
// refresh MDI Window menu (even if non-shared menu)
::SendMessage(pFrame->m_hWndMDIClient, WM_MDIREFRESHMENU, 0, 0);
}
}
需要特別注意的是:如果App類的InitInstance函數中,指定command line命令爲FileNothing(默認爲FileNew):
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
cmdInfo.m_nShellCommand = CCommandLineInfo::FileNothing;
那麼程序啓動時,是不會調用CMDIChildWnd::OnUpdateFrameMenu的。這是因爲,在創建主窗口時,已經把CFrameWnd::m_hMenuDefault,當作了Create的參數,成爲主窗口默認菜單了,所有不需要調用CMDIChildWnd::OnUpdateFrameMenu。在LibUIDK中,由於CUIWnd不需要默認菜單,所以爲了更新menu bar,應該調用CIUIMDIChildWnd::OnUpdateFrameMenu。或者CIUIMDIChildWnd::OnUpdateFrameMenu的簡化代碼:
// destroying last child
HMENU hMenuLast = NULL;
::SendMessage(pFrame->m_hWndMDIClient, WM_MDISETMENU,
(WPARAM)pFrame->m_hMenuDefault, (LPARAM)hMenuLast);
由於顯示MenuBar,必須在CMenuBar有效後進行,而CMenuBar,是在LibUIDK外部進行的。所以LibUIDK的做法是:
在CIUIMDIFrameWnd::OnCreate中,調用簡化代碼:
HMENU hMenuLast = NULL;
m_wndMDIClient.SendMessage(WM_MDISETMENU, (WPARAM)m_hMenuDefault, (LPARAM)hMenuLast);
////////////////////////////////////////////////////////////////////////////////
派生關係如下:
CFrameWnd
CMDIFrameWnd
CMDIChildWnd