MFC OnMeasureItem & OnDrawItem in menu of MDI multidoc application

414 views Asked by At

(Update, see original question below)

After doing a bit of digging, I'm basically trying to understand the following; In the context of an MDI application, if a menu (which is associated with a specific CChildWnd) has an MF_OWNERDRAW, why are the ON_WM_MEASUREITEM and ON_WM_DRAWITEM events send to the CMainWnd instead of the CChildWnd?

In my InitInstance, the document template is registered and the associated menu is modified to add the MF_OWNERDRAW:

BOOL CMyApp::InitInstance()
{
  // ...

  CMultiDocTemplate* pDocTemplate;
  pDocTemplate = new CMultiDocTemplate(
    IDR_CHILDFRAME,
    RUNTIME_CLASS(CFooDoc),
    RUNTIME_CLASS(CFooWnd),
    RUNTIME_CLASS(CFooView)
  );
    
  if (pDocTemplate->m_hMenuShared != NULL) {
    CMenu* pMenu = CMenu::FromHandle(pDocTemplate->m_hMenuShared);

    // Add MF_ONWERDRAW to the items that need it.
    pMenu->ModifyMenu([item_id], MF_BYCOMMAND | MF_OWNERDRAW, [item_id]);
  }
    
  AddDocTemplate(pDocTemplate);

  // ...
}

So, once the document template is registered, the menu associated with the document/frame is modified to add the MF_ONWERDRAW flag to each of the required items (the color selection items in my case).

However, why are the OnMeasureItem and OnDrawItem events send to the CMainWnd and not the CFooWnd? And how can I direct the events to the CFooWnd instead?

The reason I'am asking, if I have 5 different types of documents in my MDI application, each needing custom menus, then the CMainWnd basically becomes a mess of message handling. The logical place for the custom menu logic is in the CChildWnd, not the CMainWnd.

Original question:

I'm doing some work on a very old application (MFC 4.2) and I'm running into a problem with drawing in a menu item.

The original application has a menu to select a color and it actually draws the colors in the menu when opened so it easier for the user to select the color.

The behavior for this implemented in CMainWnd using the OnMeasureItem and the OnDrawItem.

class CMainWnd : public CMDIFrameWnd
{
    DECLARE_DYNCREATE(CMainWnd)
    
protected:
    afx_msg void OnMeasureItem(int, LPMEASUREITEMSTRUCT);
    afx_msg void OnDrawItem(int, LPDRAWITEMSTRUCT);
    
    DECLARE_MESSAGE_MAP()
};

Then, in the implementation (omitted bits and pieces for brevity):

BEGIN_MESSAGE_MAP(CMainWnd, CMDIFrameWnd)
    ON_WM_MEASUREITEM()
    ON_WM_DRAWITEM()
END_MESSAGE_MAP()

void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
  lpmis->itemWidth  = ::GetSystemMetrics(SM_CYMENU) * 4;
  lpmis->itemHeight = ::GetSystemMetrics(SM_CYMENU) * 1;
}

void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
  CDC dc;
  dc.Attach(lpdis->hDC);
    
  CBrush* pBrush;
    
  // draw the hover/selection rectangle
  pBrush = new CBrush(::GetSysColor((lpdis->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHT : 
  COLOR_MENU));
  dc.FrameRect(&(lpdis->rcItem), pBrush);
  delete pBrush;
    
  // load a checkbox icon into a bitmap
  BITMAP bm;
  CBitmap bitmap;
  bitmap.LoadOEMBitmap(OBM_CHECK);
  bitmap.GetObject(sizeof(bm), &bm);
    
  // if color/item selected then draw the checkbox
  if (lpdis->itemState & ODS_CHECKED) {
    CDC dcMem;
    
    dcMem.CreateCompatibleDC(&dc);
    CBitmap* pOldBitmap = dcMem.SelectObject(&bitmap);
    
    dc.BitBlt(
        lpdis->rcItem.left + 4,
        lpdis->rcItem.top + (((lpdis->rcItem.bottom - lpdis->rcItem.top) - bm.bmHeight) / bm.bmWidth,
        bm.bmHeight,
        &dcMem,
        0,
        0,
        SRCCOPY
    );
    
    dcMem.SelectObject(pOldBitmap);
  }
    
  // draw the actual color bar
  pBrush = new CBrush(CPaintDoc::m_crColors[lpdis->itemID - ID_COLOR_BLACK]);
  CRect rect = lpdis->rcItem;
  rect.DeflateRect(6, 4);
  rect.left += bm.bmWidth;
  dc.FillRect(rect, pBrush);
  delete pBrush;
    
  dc.Detach();
}

What the OnDrawItem does is; it draws a horizontal color bar with a color, prefixed by a check icon if that color is selected and the menu item being hovered over is highlighted by a box being drawn around it.

However, since I'm turning this application into a Multidoc application and I don't really feel that this logic should be in the CMainWnd (since none of the other documents will have this type of menu), but that it should be part of the CChildWnd (which inherits from CMDIChildWnd).

But when I move this logic to that class, when I run the application, I get following message in the console logger:

Warning: unknown WM_MEASUREITEM for menu item 0x0082.

And none of the custom menu behavior seems to work.

so, the question is; How can move the custom behavior of a menu into the frame class of an MDI document rather than having it located in the application main frame?

1

There are 1 answers

0
Luke On

I figured out a work around. Not ideal but I can understand that this is a quirk in the framework, i.e. the menu seems to be part of the MainWnd so from a technical point of view, that is where the ON_WM_MEASUREITEM and ON_WM_DRAWITEM would be handled.

Anyhow, my work around. Basically capture the events in the MainWnd and then delegate the behaviour to the ChildWnd. The trick here (I guess) is to figure out what ChildWnd to delegate to since in an MDI application there can be any number of different ChildWnd's (each with their own Document and View types).

The work around:

void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
    CMDIChildWnd* pActiveWnd = MDIGetActive();

    if(pActiveWnd && pActiveWnd->IsWindowVisible())
    {
      if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
         CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;

         CMyChildWnd->DoMeasureItem(nIDCtl, lpmis);
      }
   }
}

void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
    CMDIChildWnd* pActiveWnd = MDIGetActive();

    if(pActiveWnd && pActiveWnd->IsWindowVisible())
    {
      if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
         CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;

         CMyChildWnd->DoDrawItem(nIDCtl, lpdis);
      }
   }
}

Pretty straight forward, in the context of the MainWnd, get a pointer to the active MDI ChildWnd, check if it is active, then check the type by using IsKindOf and RUNTIME_CLASS and if so, voila, delegate the behavior to the ChildWnd. To DoMeasureItem and the DoDrawItem are just public methods implemented on the ChildWnd (see question for details).