【LibUIDK界面庫系列文章】MDI菜單機制



作者:劉樹偉


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章